ext_ooor 2.3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +354 -0
- data/Rakefile +5 -0
- data/bin/ooor +43 -0
- data/lib/ext_ooor.rb +5 -0
- data/lib/ext_ooor/version.rb +5 -0
- data/lib/generators/ooor/install_generator.rb +18 -0
- data/lib/generators/ooor/ooor.yml +49 -0
- data/lib/ooor.rb +230 -0
- data/lib/ooor/associations.rb +78 -0
- data/lib/ooor/autosave_association.rb +197 -0
- data/lib/ooor/base.rb +130 -0
- data/lib/ooor/base64.rb +20 -0
- data/lib/ooor/callbacks.rb +18 -0
- data/lib/ooor/errors.rb +120 -0
- data/lib/ooor/field_methods.rb +213 -0
- data/lib/ooor/helpers/core_helpers.rb +83 -0
- data/lib/ooor/locale.rb +11 -0
- data/lib/ooor/mini_active_resource.rb +86 -0
- data/lib/ooor/model_registry.rb +24 -0
- data/lib/ooor/model_schema.rb +25 -0
- data/lib/ooor/naming.rb +92 -0
- data/lib/ooor/nested_attributes.rb +57 -0
- data/lib/ooor/persistence.rb +353 -0
- data/lib/ooor/rack.rb +137 -0
- data/lib/ooor/railtie.rb +27 -0
- data/lib/ooor/reflection.rb +151 -0
- data/lib/ooor/reflection_ooor.rb +121 -0
- data/lib/ooor/relation.rb +204 -0
- data/lib/ooor/relation/finder_methods.rb +153 -0
- data/lib/ooor/report.rb +53 -0
- data/lib/ooor/serialization.rb +49 -0
- data/lib/ooor/services.rb +134 -0
- data/lib/ooor/session.rb +250 -0
- data/lib/ooor/session_handler.rb +66 -0
- data/lib/ooor/transport.rb +34 -0
- data/lib/ooor/transport/json_client.rb +65 -0
- data/lib/ooor/transport/xml_rpc_client.rb +15 -0
- data/lib/ooor/type_casting.rb +223 -0
- data/lib/ooor/version.rb +8 -0
- data/spec/cli_spec.rb +129 -0
- data/spec/helpers/test_helper.rb +11 -0
- data/spec/ooor_spec.rb +867 -0
- metadata +118 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
# OOOR: OpenObject On Ruby
|
2
|
+
# Copyright (C) 2009-2012 Akretion LTDA (<http://www.akretion.com>).
|
3
|
+
# Author: Akretion: Raphaël Valyi: CampToCamp: Nicolas Bessi, Joel Grand-Guillaume
|
4
|
+
# Licensed under the MIT license, see MIT-LICENSE file
|
5
|
+
|
6
|
+
Ooor.xtend('ir.module.module') do
|
7
|
+
|
8
|
+
##########################################################################
|
9
|
+
# Get recursively the whole list of modules dependencies
|
10
|
+
# for a list of modules.
|
11
|
+
# Do not add the module if it already exists in the input list
|
12
|
+
# Input :
|
13
|
+
# - modules : A [] of valid IrModuleModule instances with dependencies_id attribute
|
14
|
+
# Return
|
15
|
+
# - [] of dependencies
|
16
|
+
# Usage Example:
|
17
|
+
# dependency_modules = get_dependencies(modules)
|
18
|
+
def self.get_dependencies(modules)
|
19
|
+
dependency_modules = []
|
20
|
+
modules.select { |m| m.dependencies_id }.each do |mod|
|
21
|
+
mod.dependencies_id.each do |dep|
|
22
|
+
dep_module = self.find(:first,
|
23
|
+
:domain => [['name', '=', dep.name]],
|
24
|
+
:fields => ['id', 'state', 'dependencies_id'])
|
25
|
+
if dep_module.nil?
|
26
|
+
raise RuntimeError, "#{dep.name} not found"
|
27
|
+
end
|
28
|
+
dependency_modules << dep_module unless (modules + dependency_modules).map { |m| m.id }.include? dep_module.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
dependency_modules.concat(get_dependencies(dependency_modules)) if dependency_modules.count > 0
|
32
|
+
dependency_modules.uniq { |m| m.id }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.install_modules(modules)
|
36
|
+
modules = modules.map { |name| self.find(:first, domain: {name: name})}
|
37
|
+
modules.each do |mod|
|
38
|
+
mod.button_install unless mod.state == "installed"
|
39
|
+
end
|
40
|
+
wizard = BaseModuleUpgrade.create
|
41
|
+
wizard.upgrade_module
|
42
|
+
end
|
43
|
+
|
44
|
+
def print_dependency_graph
|
45
|
+
modules = [self] + self.class.get_dependencies([self])
|
46
|
+
|
47
|
+
File.open("#{self.name}-pre.dot", 'w') do |f|
|
48
|
+
f << <<-eos
|
49
|
+
digraph DependenciesByOOOR {
|
50
|
+
fontname = "Helvetica"
|
51
|
+
fontsize = 11
|
52
|
+
label = "*** generated by OOOR by www.akretion.com ***"
|
53
|
+
node [
|
54
|
+
fontname = "Helvetica"
|
55
|
+
fontsize = 11
|
56
|
+
shape = "record"
|
57
|
+
fillcolor=orange
|
58
|
+
style="rounded,filled"
|
59
|
+
]
|
60
|
+
eos
|
61
|
+
|
62
|
+
modules.each do |m|
|
63
|
+
m.dependencies_id.each do |dep|
|
64
|
+
f << "#{m.name} -> #{dep.name};\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
f << "}"
|
68
|
+
end
|
69
|
+
system("tred < #{self.name}-pre.dot > #{self.name}.dot")
|
70
|
+
cmd_line2 = "dot -Tcmapx -o#{self.name}.map -Tpng -o#{self.name}.png #{self.name}.dot"
|
71
|
+
system(cmd_line2)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
Ooor.xtend('ir.ui.menu') do
|
78
|
+
def menu_action
|
79
|
+
#TODO put in cache eventually:
|
80
|
+
action_values = self.class.ooor.const_get('ir.values').rpc_execute('get', 'action', 'tree_but_open', [['ir.ui.menu', id]], false, self.class.ooor.web_session)[0][2]#get already exists
|
81
|
+
@menu_action = self.class.ooor.const_get('ir.actions.act_window').new(action_values, []) #TODO deal with action reference instead
|
82
|
+
end
|
83
|
+
end
|
data/lib/ooor/locale.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Ooor
|
2
|
+
module Locale
|
3
|
+
# Odoo requires a locale+zone mapping while Rails uses locale only, so mapping is likely to be required
|
4
|
+
def self.to_erp_locale(locale)
|
5
|
+
unless mapping = Ooor.default_config[:locale_mapping]
|
6
|
+
mapping = {'fr' => 'fr_FR', 'en' => 'en_US'}
|
7
|
+
end
|
8
|
+
(mapping[locale.to_s] || locale.to_s).gsub('-', '_')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
if ActiveModel.respond_to?(:version) && ActiveModel.version.version.to_i >= 5
|
6
|
+
begin
|
7
|
+
require 'active_model_serializers'
|
8
|
+
require 'activemodel-serializers-xml'
|
9
|
+
rescue LoadError
|
10
|
+
puts """When using ActiveModel or Rails 5+, you should add
|
11
|
+
gem 'active_model_serializers'
|
12
|
+
gem 'activemodel-serializers-xml'
|
13
|
+
in your Gemfile.
|
14
|
+
"""
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
module Ooor
|
20
|
+
# Ooor::MiniActiveResource is a shrinked version of ActiveResource::Base with the bare minimum we need for Ooor.
|
21
|
+
# as a reminder ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
|
22
|
+
# Ooor is a bit like ActiveResource but eventually it can use more OpenERP metadata and a richer API
|
23
|
+
# to become closer to ActiveRecord or Mongoid than Activeresource. Also OpenERP isn't really good at REST
|
24
|
+
# so the part of ActiveResource dedicated to REST is of little help here.
|
25
|
+
# An other fundamental difference is Ooor is multi OpenERP instances and multi-sessions.
|
26
|
+
# for each session, proxies to OpenERP may be different.
|
27
|
+
class MiniActiveResource
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def element_name
|
31
|
+
@element_name ||= model_name.element
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_accessor :attributes, :id
|
36
|
+
|
37
|
+
def to_json(options={})
|
38
|
+
raise "you should add gem 'active_model_serializers' in your Gemfile" unless defined?(ActiveModel::Serializers::JSON)
|
39
|
+
super(include_root_in_json ? { :root => self.class.element_name }.merge(options) : options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_xml(options={})
|
43
|
+
raise "you should add gem 'activemodel-serializers-xml' in your Gemfile" unless defined?(ActiveModel::Serializers::Xml)
|
44
|
+
super({ :root => self.class.element_name }.merge(options))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns +true+ if this object hasn't yet been saved, otherwise, returns +false+.
|
48
|
+
def new?
|
49
|
+
!@persisted
|
50
|
+
end
|
51
|
+
alias :new_record? :new?
|
52
|
+
|
53
|
+
def persisted?
|
54
|
+
@persisted
|
55
|
+
end
|
56
|
+
|
57
|
+
def id
|
58
|
+
attributes["id"]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets the <tt>\id</tt> attribute of the resource.
|
62
|
+
def id=(id)
|
63
|
+
attributes["id"] = id.to_i # NOTE added to_i for Ooor (HTML forms can pass it as a string)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Reloads the record from the database.
|
67
|
+
#
|
68
|
+
# This method finds record by its primary key (which could be assigned manually) and
|
69
|
+
# modifies the receiver in-place
|
70
|
+
# NOTE in Ooor, like ActiveRecord and unlike ActiveResource, reload can take an options parameter
|
71
|
+
def reload(options = nil)
|
72
|
+
self.class.find(id, options)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the Errors object that holds all information about attribute error messages.
|
76
|
+
def errors
|
77
|
+
@errors ||= ActiveModel::Errors.new(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
include ActiveModel::Conversion
|
82
|
+
include ActiveModel::Serializers::JSON if defined?(ActiveModel::Serializers::JSON)
|
83
|
+
include ActiveModel::Serializers::Xml if defined?(ActiveModel::Serializers::Xml)
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ooor
|
2
|
+
# Enables to cache expensive model metadata and reuse these metadata
|
3
|
+
# according to connection parameters. Indeed, these metadata are
|
4
|
+
# expensive before they require a fields_get request to OpenERP
|
5
|
+
# so in a web application with several worker processes, it's a good
|
6
|
+
# idea to cache them and share them using a data store like Memcache
|
7
|
+
class ModelRegistry
|
8
|
+
|
9
|
+
def cache_key(config, model_name)
|
10
|
+
h = {url: config[:url], database: config[:database], username: config[:username], scope_prefix: config[:scope_prefix]}
|
11
|
+
(h.map{|k, v| v} + [model_name]).join('-')
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_template(config, model_name)
|
15
|
+
Ooor.cache.read(cache_key(config, model_name))
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_template(config, model)
|
19
|
+
key = cache_key(config, model.openerp_model)
|
20
|
+
Ooor.cache.write(key, model)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# OOOR: OpenObject On Ruby
|
2
|
+
# Copyright (C) 2009-2014 Akretion LTDA (<http://www.akretion.com>).
|
3
|
+
# Author: Raphaël Valyi
|
4
|
+
# Licensed under the MIT license, see MIT-LICENSE file
|
5
|
+
|
6
|
+
module Ooor
|
7
|
+
|
8
|
+
# Meta data shared across sessions, a cache of the data in ir_model in OpenERP.
|
9
|
+
# in Activerecord, ModelSchema is a module and its properties are carried by the
|
10
|
+
# ActiveRecord object. But in Ooor we don't want do do that because the Ooor::Base
|
11
|
+
# object is different for each session, so instead we delegate the schema
|
12
|
+
# properties to some ModelSchema instance that is shared between sessions,
|
13
|
+
# reused accross workers in a multi-process web app (via memcache for instance).
|
14
|
+
class ModelSchema
|
15
|
+
|
16
|
+
TEMPLATE_PROPERTIES = [:openerp_id, :info, :access_ids, :description,
|
17
|
+
:openerp_model, :field_ids, :state, :fields,
|
18
|
+
:many2one_associations, :one2many_associations, :many2many_associations,
|
19
|
+
:polymorphic_m2o_associations, :associations_keys,
|
20
|
+
:associations, :columns]
|
21
|
+
|
22
|
+
attr_accessor *TEMPLATE_PROPERTIES, :name, :columns_hash
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/ooor/naming.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Ooor
|
4
|
+
module Naming
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class Name < ActiveModel::Name
|
8
|
+
def initialize(klass, namespace = nil, name = nil)
|
9
|
+
super
|
10
|
+
@singular = klass.openerp_model
|
11
|
+
@plural = klass.openerp_model # OpenERP doesn't enforce plural / singular conventions sadly...
|
12
|
+
@element = klass.openerp_model
|
13
|
+
@human = klass.description || klass.openerp_model
|
14
|
+
@param_key = klass.openerp_model.gsub('.', '_')
|
15
|
+
@i18n_key = klass.openerp_model
|
16
|
+
@route_key = klass.openerp_model.gsub('.', '-')
|
17
|
+
@singular_route_key = klass.openerp_model.gsub('.', '-')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def model_name
|
23
|
+
@_model_name ||= begin
|
24
|
+
if self.respond_to?(:openerp_model) && self.t
|
25
|
+
namespace = self.parents.detect do |n|
|
26
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
27
|
+
end
|
28
|
+
Ooor::Naming::Name.new(self, namespace)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def param_key(context={})
|
36
|
+
self.alias(context).gsub('.', '-') # we don't use model_name because model_name isn't bijective
|
37
|
+
end
|
38
|
+
|
39
|
+
#similar to Object#const_get but for OpenERP model key
|
40
|
+
def const_get(model_key)
|
41
|
+
scope = self.scope_prefix ? Object.const_get(self.scope_prefix) : Object
|
42
|
+
klass_name = session.class_name_from_model_key(model_key)
|
43
|
+
if scope.const_defined?(klass_name) && Ooor.session_handler.noweb_session_spec(scope.const_get(klass_name).session.config) == Ooor.session_handler.noweb_session_spec(session.config)
|
44
|
+
scope.const_get(klass_name)
|
45
|
+
else
|
46
|
+
session.define_openerp_model(model: model_key, scope_prefix: self.scope_prefix)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#required by form validators; TODO implement better?
|
51
|
+
def human_attribute_name(field_name, options={})
|
52
|
+
""
|
53
|
+
end
|
54
|
+
|
55
|
+
def param_field
|
56
|
+
session.config[:param_keys] && session.config[:param_keys][openerp_model] || :id
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_by_permalink(param, options={})
|
60
|
+
# NOTE in v8, see if we can use PageConverter here https://github.com/akretion/openerp-addons/blob/trunk-website-al/website/models/ir_http.py#L138
|
61
|
+
if param.split('-').last.to_i != 0
|
62
|
+
options.merge!(domain: {:id => param.split('-').last.to_i})
|
63
|
+
elsif param.to_i == 0
|
64
|
+
options.merge!(domain: [param_field, 'ilike', param])
|
65
|
+
else
|
66
|
+
options.merge!(domain: {:id => param.to_i})
|
67
|
+
end
|
68
|
+
find(:first, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def alias(context={})
|
72
|
+
# NOTE in v8, see if we can use ModelConvert here https://github.com/akretion/openerp-addons/blob/trunk-website-al/website/models/ir_http.py#L126
|
73
|
+
if session.config[:aliases]
|
74
|
+
lang = context['lang'] || session.config['lang'] || 'en_US'
|
75
|
+
if alias_data = session.config[:aliases][lang]
|
76
|
+
alias_data.select{|key, value| value == openerp_model }.keys[0] || openerp_model
|
77
|
+
else
|
78
|
+
openerp_model
|
79
|
+
end
|
80
|
+
else
|
81
|
+
openerp_model
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_param
|
87
|
+
field = self.class.param_field
|
88
|
+
send(field) && send(field).to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module Ooor
|
5
|
+
module NestedAttributes #:nodoc:
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Defines an attributes writer for the specified association(s).
|
11
|
+
# Note that in Ooor this is active by default for all one2many and many2one associations
|
12
|
+
def accepts_nested_attributes_for(*attr_names)
|
13
|
+
attr_names.each do |association_name|
|
14
|
+
if rel = all_fields[association_name]
|
15
|
+
reflection = OpenStruct.new(rel.merge({options: {autosave: true}, name: association_name})) #TODO use a reflection class
|
16
|
+
generate_association_writer(association_name, :collection) #TODO add support for m2o
|
17
|
+
add_autosave_association_callbacks(reflection)
|
18
|
+
else
|
19
|
+
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Generates a writer method for this association. Serves as a point for
|
27
|
+
# accessing the objects in the association. For example, this method
|
28
|
+
# could generate the following:
|
29
|
+
#
|
30
|
+
# def pirate_attributes=(attributes)
|
31
|
+
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# This redirects the attempts to write objects in an association through
|
35
|
+
# the helper methods defined below. Makes it seem like the nested
|
36
|
+
# associations are just regular associations.
|
37
|
+
def generate_association_writer(association_name, type)
|
38
|
+
unless self.respond_to?(association_name)
|
39
|
+
self.instance_eval do
|
40
|
+
define_method "#{association_name}_attributes=" do |*args|
|
41
|
+
send("#{association_name}_will_change!")
|
42
|
+
# @associations[association_name] = args[0] # TODO what do we do here?
|
43
|
+
association_obj = self.class.reflect_on_association(association_name).klass
|
44
|
+
associations = []
|
45
|
+
(args[0] || {}).each do |k, v|
|
46
|
+
persisted = !v['id'].blank? || v[:id]
|
47
|
+
associations << association_obj.new(v, [], persisted, true) #TODO eventually use k to set sequence
|
48
|
+
end
|
49
|
+
@loaded_associations[association_name] = associations
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,353 @@
|
|
1
|
+
# OOOR: OpenObject On Ruby
|
2
|
+
# Copyright (C) 2009-2014 Akretion LTDA (<http://www.akretion.com>).
|
3
|
+
# Author: Raphaël Valyi
|
4
|
+
# Licensed under the MIT license, see MIT-LICENSE file
|
5
|
+
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
require 'active_model/attribute_methods'
|
8
|
+
require 'active_model/dirty'
|
9
|
+
require 'ooor/errors'
|
10
|
+
|
11
|
+
module Ooor
|
12
|
+
# = Ooor RecordInvalid
|
13
|
+
#
|
14
|
+
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
|
15
|
+
# +record+ method to retrieve the record which did not validate.
|
16
|
+
#
|
17
|
+
# begin
|
18
|
+
# complex_operation_that_calls_save!_internally
|
19
|
+
# rescue ActiveRecord::RecordInvalid => invalid
|
20
|
+
# puts invalid.record.errors
|
21
|
+
# end
|
22
|
+
class RecordInvalid < OpenERPServerError
|
23
|
+
attr_reader :record # :nodoc:
|
24
|
+
def initialize(record) # :nodoc:
|
25
|
+
@record = record
|
26
|
+
errors = @record.errors.full_messages.join(", ")
|
27
|
+
super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# = Ooor Persistence
|
32
|
+
# Note that at the moment it also includes the Validations stuff as it is quite superficial in Ooor
|
33
|
+
# Most of the time, when we talk about validation here we talk about extra Rails validations
|
34
|
+
# as OpenERP validations will happen anyhow when persisting records to OpenERP.
|
35
|
+
# some of the methods found in ActiveRecord Persistence which are identical in ActiveResource
|
36
|
+
# may be found in the Ooor::MiniActiveResource module instead
|
37
|
+
module Persistence
|
38
|
+
extend ActiveSupport::Concern
|
39
|
+
include ActiveModel::Validations
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
|
44
|
+
# The resulting object is returned whether the object was saved successfully to the database or not.
|
45
|
+
#
|
46
|
+
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
|
47
|
+
# attributes on the objects that are to be created.
|
48
|
+
#
|
49
|
+
# the +default_get_list+ parameter differs from the ActiveRecord API
|
50
|
+
# it is used to tell OpenERP the list of fields for which we want the default values
|
51
|
+
# false will request all default values while [] will not ask for any default value (faster)
|
52
|
+
# +reload+ can be set to false to indicate you don't want to reload the record after it is saved
|
53
|
+
# which will save a roundtrip to OpenERP and perform faster.
|
54
|
+
def create(attributes = {}, default_get_list = false, reload = true, &block)
|
55
|
+
if attributes.is_a?(Array)
|
56
|
+
attributes.collect { |attr| create(attr, &block) }
|
57
|
+
else
|
58
|
+
object = new(attributes, default_get_list, &block)
|
59
|
+
object.save(reload)
|
60
|
+
object
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
|
65
|
+
# so an exception is raised if the record is invalid.
|
66
|
+
def create!(attributes = {}, default_get_list = false, reload = true, &block)
|
67
|
+
if attributes.is_a?(Array)
|
68
|
+
attributes.collect { |attr| create!(attr, &block) }
|
69
|
+
else
|
70
|
+
object = new(attributes, default_get_list)
|
71
|
+
yield(object) if block_given?
|
72
|
+
object.save!(reload)
|
73
|
+
object
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if this object has been destroyed, otherwise returns false.
|
80
|
+
def destroyed?
|
81
|
+
@destroyed
|
82
|
+
end
|
83
|
+
|
84
|
+
# Flushes the current object and loads the +attributes+ Hash
|
85
|
+
# containing the attributes and the associations into the current object
|
86
|
+
def load(attributes)
|
87
|
+
self.class.reload_fields_definition(false)
|
88
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
89
|
+
@associations ||= {}
|
90
|
+
@attributes ||= {}
|
91
|
+
@loaded_associations = {}
|
92
|
+
attributes.each do |key, value|
|
93
|
+
self.send "#{key}=", value if self.respond_to?("#{key}=")
|
94
|
+
end
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
#takes care of reading OpenERP default field values.
|
99
|
+
def initialize(attributes = {}, default_get_list = false, persisted = false, has_changed = false, lazy = false)
|
100
|
+
self.class.reload_fields_definition(false)
|
101
|
+
@attributes = {}
|
102
|
+
@ir_model_data_id = attributes.delete(:ir_model_data_id)
|
103
|
+
@marked_for_destruction = false
|
104
|
+
@persisted = persisted
|
105
|
+
@lazy = lazy
|
106
|
+
if default_get_list == []
|
107
|
+
load(attributes)
|
108
|
+
else
|
109
|
+
load_with_defaults(attributes, default_get_list)
|
110
|
+
end.tap do
|
111
|
+
if id && !has_changed
|
112
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new # see ActiveModel::Dirty reset_changes
|
113
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Saves the model.
|
119
|
+
#
|
120
|
+
# If the model is new a record gets created in OpenERP, otherwise
|
121
|
+
# the existing record gets updated.
|
122
|
+
#
|
123
|
+
# By default, save always run validations. If any of them fail the action
|
124
|
+
# is cancelled and +save+ returns +false+. However, if you supply
|
125
|
+
# validate: false, validations are bypassed altogether.
|
126
|
+
# In Ooor however, real validations always happen on the OpenERP side
|
127
|
+
# so the only validations you can bypass or not are extra pre-validations
|
128
|
+
# in Ruby if you have any.
|
129
|
+
#
|
130
|
+
# There's a series of callbacks associated with +save+. If any of the
|
131
|
+
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
|
132
|
+
# +save+ returns +false+. See ActiveRecord::Callbacks for further
|
133
|
+
# details.
|
134
|
+
#
|
135
|
+
# Attributes marked as readonly are silently ignored if the record is
|
136
|
+
# being updated. (TODO)
|
137
|
+
def save(options = {})
|
138
|
+
perform_validations(options) ? save_without_raising(options) : false
|
139
|
+
end
|
140
|
+
|
141
|
+
# Attempts to save the record just like save but will raise a +RecordInvalid+
|
142
|
+
# exception instead of returning +false+ if the record is not valid.
|
143
|
+
def save!(options = {})
|
144
|
+
perform_validations(options) ? save_without_raising(options) : raise(RecordInvalid.new(self))
|
145
|
+
end
|
146
|
+
|
147
|
+
# Deletes the record in OpenERP and freezes this instance to
|
148
|
+
# reflect that no changes should be made (since they can't be
|
149
|
+
# persisted). Returns the frozen instance.
|
150
|
+
#
|
151
|
+
# no callbacks are executed.
|
152
|
+
#
|
153
|
+
# To enforce the object's +before_destroy+ and +after_destroy+
|
154
|
+
# callbacks or any <tt>:dependent</tt> association
|
155
|
+
# options, use <tt>#destroy</tt>.
|
156
|
+
def delete
|
157
|
+
rpc_execute('unlink', [id], context) if persisted?
|
158
|
+
@destroyed = true
|
159
|
+
freeze
|
160
|
+
end
|
161
|
+
|
162
|
+
# Deletes the record in OpenERP and freezes this instance to reflect
|
163
|
+
# that no changes should be made (since they can't be persisted).
|
164
|
+
#
|
165
|
+
# There's a series of callbacks associated with <tt>destroy</tt>. If
|
166
|
+
# the <tt>before_destroy</tt> callback return +false+ the action is cancelled
|
167
|
+
# and <tt>destroy</tt> returns +false+. See
|
168
|
+
# ActiveRecord::Callbacks for further details.
|
169
|
+
def destroy
|
170
|
+
run_callbacks :destroy do
|
171
|
+
rpc_execute('unlink', [id], context)
|
172
|
+
@destroyed = true
|
173
|
+
freeze
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Deletes the record in the database and freezes this instance to reflect
|
178
|
+
# that no changes should be made (since they can't be persisted).
|
179
|
+
#
|
180
|
+
# There's a series of callbacks associated with <tt>destroy!</tt>. If
|
181
|
+
# the <tt>before_destroy</tt> callback return +false+ the action is cancelled
|
182
|
+
# and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
|
183
|
+
# ActiveRecord::Callbacks for further details.
|
184
|
+
def destroy! #TODO
|
185
|
+
destroy || raise(ActiveRecord::RecordNotDestroyed)
|
186
|
+
end
|
187
|
+
|
188
|
+
#TODO implement becomes / becomes! eventually
|
189
|
+
|
190
|
+
# Updates a single attribute and saves the record.
|
191
|
+
# This is especially useful for boolean flags on existing records. Also note that
|
192
|
+
#
|
193
|
+
# * Validation is skipped.
|
194
|
+
# * Callbacks are invoked.
|
195
|
+
# * updated_at/updated_on column is updated if that column is available.
|
196
|
+
# * Updates all the attributes that are dirty in this object.
|
197
|
+
#
|
198
|
+
# This method raises an +ActiveRecord::ActiveRecordError+ if the
|
199
|
+
# attribute is marked as readonly.
|
200
|
+
#
|
201
|
+
# See also +update_column+.
|
202
|
+
def update_attribute(name, value)
|
203
|
+
send("#{name}=", value)
|
204
|
+
save(validate: false)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Updates the attributes of the model from the passed-in hash and saves the
|
208
|
+
# record, all wrapped in a transaction. If the object is invalid, the saving
|
209
|
+
# will fail and false will be returned.
|
210
|
+
def update(attributes, reload=true)
|
211
|
+
load(attributes) && save(reload)
|
212
|
+
end
|
213
|
+
|
214
|
+
alias update_attributes update
|
215
|
+
|
216
|
+
# Updates its receiver just like +update+ but calls <tt>save!</tt> instead
|
217
|
+
# of +save+, so an exception is raised if the record is invalid.
|
218
|
+
def update!(attributes, reload=true)
|
219
|
+
load(attributes) && save!(reload)
|
220
|
+
end
|
221
|
+
|
222
|
+
alias update_attributes! update!
|
223
|
+
|
224
|
+
#OpenERP copy method, load persisted copied Object
|
225
|
+
def copy(defaults={}, context={})
|
226
|
+
self.class.find(rpc_execute('copy', id, defaults, context), context: context)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Runs all the validations within the specified context. Returns +true+ if
|
230
|
+
# no errors are found, +false+ otherwise.
|
231
|
+
#
|
232
|
+
# Aliased as validate.
|
233
|
+
#
|
234
|
+
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
|
235
|
+
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
|
236
|
+
#
|
237
|
+
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
|
238
|
+
# some <tt>:on</tt> option will only run in the specified context.
|
239
|
+
def valid?(context = nil)
|
240
|
+
context ||= (new_record? ? :create : :update)
|
241
|
+
output = super(context)
|
242
|
+
errors.empty? && output
|
243
|
+
end
|
244
|
+
|
245
|
+
alias_method :validate, :valid?
|
246
|
+
|
247
|
+
protected
|
248
|
+
|
249
|
+
# Real validations happens on OpenERP side, only pre-validations can happen here eventually
|
250
|
+
def perform_validations(options={}) # :nodoc:
|
251
|
+
if options.is_a?(Hash)
|
252
|
+
options[:validate] == false || valid?(options[:context])
|
253
|
+
else
|
254
|
+
valid?
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def create_or_update(options={})
|
261
|
+
run_callbacks :save do
|
262
|
+
new? ? create_record(options) : update_record(options)
|
263
|
+
end
|
264
|
+
rescue ValidationError => e
|
265
|
+
e.extract_validation_error!(errors)
|
266
|
+
return false
|
267
|
+
end
|
268
|
+
|
269
|
+
def update_record(options)
|
270
|
+
run_callbacks :update do
|
271
|
+
rpc_execute('write', [self.id], to_openerp_hash, context)
|
272
|
+
reload_fields if should_reload?(options)
|
273
|
+
@persisted = true
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def create_record(options={})
|
278
|
+
run_callbacks :create do
|
279
|
+
self.id = rpc_execute('create', to_openerp_hash, context)
|
280
|
+
if @ir_model_data_id
|
281
|
+
IrModelData.create(model: self.class.openerp_model,
|
282
|
+
'module' => @ir_model_data_id[0],
|
283
|
+
'name' => @ir_model_data_id[1],
|
284
|
+
'res_id' => self.id)
|
285
|
+
end
|
286
|
+
@persisted = true
|
287
|
+
reload_fields if should_reload?(options)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def save_without_raising(options = {})
|
292
|
+
create_or_update(options)
|
293
|
+
rescue Ooor::RecordInvalid
|
294
|
+
false
|
295
|
+
end
|
296
|
+
|
297
|
+
def should_validate?(options)
|
298
|
+
if options.is_a?(Hash)
|
299
|
+
options[:validate] != false
|
300
|
+
else
|
301
|
+
true
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def should_reload?(options)
|
306
|
+
if options == false
|
307
|
+
false
|
308
|
+
elsif options.is_a?(Hash) && options[:reload] == false
|
309
|
+
false
|
310
|
+
else
|
311
|
+
true
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def load_with_defaults(attributes, default_get_list)
|
316
|
+
defaults = rpc_execute("default_get", default_get_list || self.class.fields.keys + self.class.associations_keys, context)
|
317
|
+
self.class.associations_keys.each do |k|
|
318
|
+
# m2m with existing records:
|
319
|
+
if defaults[k].is_a?(Array) && defaults[k][0].is_a?(Array) && defaults[k][0][2].is_a?(Array)
|
320
|
+
defaults[k] = defaults[k][0][2]
|
321
|
+
# m2m with records to create:
|
322
|
+
elsif defaults[k].is_a?(Array) && defaults[k][0].is_a?(Array) && defaults[k][0][2].is_a?(Hash) # TODO make more robust
|
323
|
+
defaults[k] = defaults[k].map { |item| self.class.all_fields[k]['relation'].new(item[2]) }
|
324
|
+
# strange case with default product taxes on v9
|
325
|
+
elsif defaults[k].is_a?(Array) && defaults[k][0] == [5] && defaults[k][1].is_a?(Array)
|
326
|
+
defaults[k] = [defaults[k][1].last] # TODO may e more subtle
|
327
|
+
# default ResPartners category_id on v9; know why...
|
328
|
+
elsif defaults[k].is_a?(Array) && defaults[k][0].is_a?(Array)
|
329
|
+
defaults[k] = defaults[k][0]
|
330
|
+
end
|
331
|
+
end
|
332
|
+
attributes = HashWithIndifferentAccess.new(defaults.merge(attributes.reject {|k, v| v.blank? }))
|
333
|
+
load(attributes)
|
334
|
+
end
|
335
|
+
|
336
|
+
def load_on_change_result(result, field_name, field_value)
|
337
|
+
if result["warning"]
|
338
|
+
self.class.logger.info result["warning"]["title"]
|
339
|
+
self.class.logger.info result["warning"]["message"]
|
340
|
+
end
|
341
|
+
attrs = @attributes.merge(field_name => field_value)
|
342
|
+
attrs.merge!(result["value"])
|
343
|
+
load(attrs)
|
344
|
+
end
|
345
|
+
|
346
|
+
def reload_fields
|
347
|
+
record = self.class.find(self.id, context: context)
|
348
|
+
load(record.attributes.merge(record.associations))
|
349
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new # see ActiveModel::Dirty
|
350
|
+
end
|
351
|
+
|
352
|
+
end
|
353
|
+
end
|