ext_ooor 2.3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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