ext_ooor 2.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +354 -0
  4. data/Rakefile +5 -0
  5. data/bin/ooor +43 -0
  6. data/lib/ext_ooor.rb +5 -0
  7. data/lib/ext_ooor/version.rb +5 -0
  8. data/lib/generators/ooor/install_generator.rb +18 -0
  9. data/lib/generators/ooor/ooor.yml +49 -0
  10. data/lib/ooor.rb +230 -0
  11. data/lib/ooor/associations.rb +78 -0
  12. data/lib/ooor/autosave_association.rb +197 -0
  13. data/lib/ooor/base.rb +130 -0
  14. data/lib/ooor/base64.rb +20 -0
  15. data/lib/ooor/callbacks.rb +18 -0
  16. data/lib/ooor/errors.rb +120 -0
  17. data/lib/ooor/field_methods.rb +213 -0
  18. data/lib/ooor/helpers/core_helpers.rb +83 -0
  19. data/lib/ooor/locale.rb +11 -0
  20. data/lib/ooor/mini_active_resource.rb +86 -0
  21. data/lib/ooor/model_registry.rb +24 -0
  22. data/lib/ooor/model_schema.rb +25 -0
  23. data/lib/ooor/naming.rb +92 -0
  24. data/lib/ooor/nested_attributes.rb +57 -0
  25. data/lib/ooor/persistence.rb +353 -0
  26. data/lib/ooor/rack.rb +137 -0
  27. data/lib/ooor/railtie.rb +27 -0
  28. data/lib/ooor/reflection.rb +151 -0
  29. data/lib/ooor/reflection_ooor.rb +121 -0
  30. data/lib/ooor/relation.rb +204 -0
  31. data/lib/ooor/relation/finder_methods.rb +153 -0
  32. data/lib/ooor/report.rb +53 -0
  33. data/lib/ooor/serialization.rb +49 -0
  34. data/lib/ooor/services.rb +134 -0
  35. data/lib/ooor/session.rb +250 -0
  36. data/lib/ooor/session_handler.rb +66 -0
  37. data/lib/ooor/transport.rb +34 -0
  38. data/lib/ooor/transport/json_client.rb +65 -0
  39. data/lib/ooor/transport/xml_rpc_client.rb +15 -0
  40. data/lib/ooor/type_casting.rb +223 -0
  41. data/lib/ooor/version.rb +8 -0
  42. data/spec/cli_spec.rb +129 -0
  43. data/spec/helpers/test_helper.rb +11 -0
  44. data/spec/ooor_spec.rb +867 -0
  45. 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
@@ -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
@@ -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