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,130 @@
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/module/delegation.rb'
7
+ require 'active_model/attribute_methods'
8
+ require 'active_model/dirty'
9
+ require 'ooor/reflection'
10
+ require 'ooor/reflection_ooor'
11
+ require 'ooor/errors'
12
+
13
+ module Ooor
14
+
15
+ # the base class for proxies to OpenERP objects
16
+ class Base < Ooor::MiniActiveResource
17
+ include Naming, TypeCasting, Serialization, ReflectionOoor, Reflection
18
+ include Associations, Report, FinderMethods, FieldMethods, AutosaveAssociation, NestedAttributes
19
+
20
+ # ********************** class methods ************************************
21
+ class << self
22
+
23
+ attr_accessor :name, :session, :t, :scope_prefix
24
+ delegate *ModelSchema::TEMPLATE_PROPERTIES, to: :t
25
+
26
+ # ******************** remote communication *****************************
27
+
28
+ #OpenERP search method
29
+ def search(domain=[], offset=0, limit=false, order=false, context={}, count=false)
30
+ rpc_execute(:search, to_openerp_domain(domain), offset, limit, order, context, count)
31
+ end
32
+
33
+ def name_search(name='', domain=[], operator='ilike', limit=100, context={})
34
+ if session.odoo_serie < 10
35
+ rpc_execute(:name_search, name, to_openerp_domain(domain), operator, context, limit)
36
+ else
37
+ rpc_execute(:name_search, name, to_openerp_domain(domain), operator, limit)
38
+ end
39
+ end
40
+
41
+ def rpc_execute(method, *args)
42
+ object_service(:execute, openerp_model, method, *args)
43
+ end
44
+
45
+ def rpc_exec_workflow(action, *args)
46
+ object_service(:exec_workflow, openerp_model, action, *args)
47
+ end
48
+
49
+ def object_service(service, obj, method, *args)
50
+ reload_fields_definition(false)
51
+ cast_answer_to_ruby!(session.object.object_service(service, obj, method, *cast_request_to_openerp(args)))
52
+ end
53
+
54
+ def context
55
+ session.session_context
56
+ end
57
+
58
+ def method_missing(method_symbol, *args)
59
+ raise RuntimeError.new("Invalid RPC method: #{method_symbol}") if [:type!, :allowed!].index(method_symbol)
60
+ self.rpc_execute(method_symbol.to_s, *args)
61
+ end
62
+
63
+ # ******************** AREL Minimal implementation ***********************
64
+
65
+ def where(opts, *rest); relation.where(opts, *rest); end
66
+ def all(*args); relation.all(*args); end
67
+ def limit(value); relation.limit(value); end
68
+ def order(value); relation.order(value); end
69
+ def offset(value); relation.offset(value); end
70
+ def first(*args); relation.first(*args); end
71
+ def last(*args); relation.last(*args); end
72
+ def includes(*args); relation.includes(*args); end
73
+
74
+
75
+ def logger; Ooor.logger; end
76
+
77
+ private
78
+
79
+ def relation; @relation ||= Relation.new(self); end
80
+
81
+ end
82
+
83
+ self.name = "Base"
84
+
85
+ # ********************** instance methods **********************************
86
+
87
+ attr_accessor :associations, :loaded_associations, :ir_model_data_id
88
+
89
+ include Persistence, Callbacks, ActiveModel::Dirty
90
+
91
+ def rpc_execute(method, *args)
92
+ args += [self.class.context] unless args[-1].is_a? Hash
93
+ self.class.object_service(:execute, self.class.openerp_model, method, *args)
94
+ end
95
+
96
+ #Generic OpenERP rpc method call
97
+ def call(method, *args) rpc_execute(method, *args) end
98
+
99
+ #Generic OpenERP on_change method
100
+ def on_change(on_change_method, field_name, field_value, *args)
101
+ # NOTE: OpenERP doesn't accept context systematically in on_change events unfortunately
102
+ ids = self.id ? [id] : []
103
+ result = self.class.object_service(:execute, self.class.openerp_model, on_change_method, ids, *args)
104
+ load_on_change_result(result, field_name, field_value)
105
+ end
106
+
107
+ #wrapper for OpenERP exec_workflow Business Process Management engine
108
+ def wkf_action(action, context={}, reload=true)
109
+ self.class.object_service(:exec_workflow, self.class.openerp_model, action, self.id, context)
110
+ reload_fields if reload
111
+ end
112
+
113
+ #Add get_report_data to obtain [report["result"],report["format]] of a concrete openERP Object
114
+ def get_report_data(report_name, report_type="pdf", context={})
115
+ self.class.get_report_data(report_name, [self.id], report_type, context)
116
+ end
117
+
118
+ def type() method_missing(:type) end #skips deprecated Object#type method
119
+
120
+ private
121
+
122
+ def context
123
+ self.class.context
124
+ end
125
+
126
+ # Ruby 1.9.compat, See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
127
+ def to_ary; nil; end # :nodoc:
128
+
129
+ end
130
+ end
@@ -0,0 +1,20 @@
1
+ # OOOR: OpenObject On Ruby
2
+ # Copyright (C) 2009-2012 Akretion LTDA (<http://www.akretion.com>).
3
+ # Author: Raphaël Valyi
4
+ # Licensed under the MIT license, see MIT-LICENSE file
5
+
6
+ autoload :Base64, 'base64'
7
+
8
+ module Base64
9
+ def serialize_binary_from_file(binary_path)
10
+ return Base64.encode64(File.read(binary_path))
11
+ end
12
+
13
+ def serialize_binary_from_content(content)
14
+ return Base64.encode64(content)
15
+ end
16
+
17
+ def unserialize_binary(content)
18
+ return Base64.decode64(content)
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ module Ooor
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ CALLBACKS = [
6
+ :before_validation, :after_validation, :before_save, :around_save, :after_save,
7
+ :before_create, :around_create, :after_create, :before_update, :around_update,
8
+ :after_update, :before_destroy, :around_destroy, :after_destroy
9
+ ]
10
+
11
+ included do
12
+ extend ActiveModel::Callbacks
13
+ include ActiveModel::Validations::Callbacks
14
+
15
+ define_model_callbacks :save, :create, :update, :destroy
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,120 @@
1
+ module Ooor
2
+ class OpenERPServerError < RuntimeError
3
+ attr_accessor :request, :faultCode, :faultString
4
+
5
+ def self.create_from_trace(error, method, *args)
6
+ begin
7
+ #extracts the eventual error log from OpenERP response as OpenERP doesn't enforce carefully*
8
+ #the XML/RPC spec, see https://bugs.launchpad.net/openerp/+bug/257581
9
+ openerp_error_hash = eval("#{error}".gsub("wrong fault-structure: ", ""))
10
+ rescue SyntaxError
11
+ end
12
+ if openerp_error_hash.is_a? Hash
13
+ build(openerp_error_hash['faultCode'], openerp_error_hash['faultString'], method, *args)
14
+ else
15
+ return UnknownOpenERPServerError.new("method: #{method} - args: #{args.inspect}")
16
+ end
17
+ end
18
+
19
+ def self.build(faultCode, faultString, method, *args)
20
+ if faultCode =~ /AttributeError: / || faultCode =~ /object has no attribute/
21
+ return UnknownAttributeOrAssociationError.new("method: #{method} - args: #{args.inspect}", faultCode, faultString)
22
+ elsif faultCode =~ /TypeError: /
23
+ return TypeError.new(method, faultCode, faultString, *args)
24
+ elsif faultCode =~ /ValueError: /
25
+ return ValueError.new(method, faultCode, faultString, *args)
26
+ elsif faultCode =~ /ValidateError/
27
+ return ValidationError.new(method, faultCode, faultString, *args)
28
+ elsif faultCode =~ /AccessDenied/ || faultCode =~ /Access Denied/ || faultCode =~ /AccessError/
29
+ return UnAuthorizedError.new(method, faultCode, faultString, *args)
30
+ elsif faultCode =~ /AuthenticationError: Credentials not provided/
31
+ return InvalidSessionError.new(method, faultCode, faultString, *args)
32
+ elsif faultCode =~ /SessionExpiredException/
33
+ return SessionExpiredError.new(method, faultCode, faultString, *args)
34
+ else
35
+ return new(method, faultCode, faultString, *args)
36
+ end
37
+ end
38
+
39
+ def initialize(method=nil, faultCode=nil, faultString=nil, *args)
40
+ filtered_args = filter_password(args.dup())
41
+ @request = "method: #{method} - args: #{filtered_args.inspect}"
42
+ @faultCode = faultCode
43
+ @faultString = faultString
44
+ super()
45
+ end
46
+
47
+ def filter_password(args)
48
+ if args[0].is_a?(String) && args[2].is_a?(String) && (args[1].is_a?(Integer) || (args[1].respond_to?(:to_i) && args[1].to_i != 0))
49
+ args[2] = "####"
50
+ end
51
+ args.map! do |arg|
52
+ if arg.is_a?(Hash)# && (arg.keys.index('password') || arg.keys.index(:password))
53
+ r = {}
54
+ arg.each do |k, v|
55
+ if k.to_s.index('password')
56
+ r[k] = "####"
57
+ else
58
+ r[k] = v
59
+ end
60
+ end
61
+ r
62
+ else
63
+ arg
64
+ end
65
+ end
66
+ end
67
+
68
+ def to_s()
69
+ s = super
70
+ line = "********************************************"
71
+ s = "\n\n#{line}\n*********** OOOR Request ***********\n#{@request}\n#{line}\n\n"
72
+ s << "\n#{line}\n*********** OpenERP Server ERROR ***********\n#{line}\n#{@faultCode}\n#{@faultString}\n#{line}\n."
73
+ s
74
+ end
75
+
76
+ end
77
+
78
+
79
+ class UnknownOpenERPServerError < OpenERPServerError; end
80
+ class UnAuthorizedError < OpenERPServerError; end
81
+ class TypeError < OpenERPServerError; end
82
+ class ValueError < OpenERPServerError; end
83
+ class InvalidSessionError < OpenERPServerError; end
84
+ class SessionExpiredError < OpenERPServerError; end
85
+
86
+ class ValidationError < OpenERPServerError
87
+ def extract_validation_error!(errors)
88
+ @faultCode.split("\n").each do |line|
89
+ extract_error_line!(errors, line) if line.index(': ')
90
+ end
91
+ end
92
+
93
+ def extract_error_line!(errors, line)
94
+ fields = line.split(": ")[0].split(' ').last.split(',')
95
+ msg = line.split(": ")[1]
96
+ fields.each { |field| errors.add(field.strip.to_sym, msg) }
97
+ end
98
+ end
99
+
100
+ class UnknownAttributeOrAssociationError < OpenERPServerError
101
+ attr_accessor :klass
102
+
103
+ def to_s()
104
+ s = super
105
+ s << available_fields(@klass) if @klass
106
+ s
107
+ end
108
+
109
+ def available_fields(clazz)
110
+ msg = "\n\n*** AVAILABLE FIELDS ON #{clazz.name} ARE: ***"
111
+ msg << "\n\n" << clazz.t.fields.sort {|a,b| a[1]['type']<=>b[1]['type']}.map {|i| "#{i[1]['type']} --- #{i[0]}"}.join("\n")
112
+ %w[many2one one2many many2many polymorphic_m2o].each do |kind|
113
+ msg << "\n\n"
114
+ msg << (clazz.send "#{kind}_associations").map {|k, v| "#{kind} --- #{v['relation']} --- #{k}"}.join("\n")
115
+ end
116
+ msg
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,213 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ooor
4
+ module FieldMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def reload_fields_definition(force=false)
10
+ if force || !fields
11
+ @t.fields = {}
12
+ @columns_hash = {}
13
+ if session.odoo_serie < 10
14
+ fields_get = rpc_execute("fields_get", false, context)
15
+ else
16
+ fields_get = rpc_execute("fields_get", false)
17
+ end
18
+ fields_get.each { |k, field| reload_field_definition(k, field) }
19
+ @t.associations_keys = many2one_associations.keys + one2many_associations.keys + many2many_associations.keys + polymorphic_m2o_associations.keys
20
+ logger.debug "#{fields.size} fields loaded in model #{self.name}"
21
+ Ooor.model_registry.set_template(session.config, @t)
22
+ end
23
+ generate_accessors if fields != {} && (force || !@accessor_defined) #TODOmove in define_accessors method
24
+ end
25
+
26
+ def all_fields
27
+ fields.merge(polymorphic_m2o_associations).merge(many2many_associations).merge(one2many_associations).merge(many2one_associations)
28
+ end
29
+
30
+ def fast_fields(options={})
31
+ fields = all_fields
32
+ fields.keys.select do |k|
33
+ fields[k]["type"] != "binary" && (options[:include_functions] || !fields[k]["function"])
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def generate_accessors #TODO we should cache this is a module cached like the template, or eventually generate source code or both
40
+ fields.keys.each { |meth| define_field_method meth }
41
+ associations_keys.each { |meth| define_association_method meth }
42
+ one2many_associations.keys.each { |meth| accepts_nested_attributes_for meth } #TODO do it for m2o too
43
+ many2one_associations.keys.each do |meth|
44
+ define_association_method meth
45
+ define_m2o_association_method meth
46
+ end
47
+ (one2many_associations.keys + many2many_associations.keys).each do |meth|
48
+ define_association_method meth
49
+ alias_method "#{meth}_ids=", "#{meth}="
50
+ alias_method "#{meth.to_s.singularize}_ids=", "#{meth}="
51
+ define_x2m_ids_association_method meth
52
+ alias_method "#{meth.to_s.singularize}_ids", "#{meth}_ids"
53
+ end
54
+ @accessor_defined = true
55
+ end
56
+
57
+ def define_field_method(meth)
58
+ define_attribute_method meth
59
+ define_method meth do |*args|
60
+ get_attribute(meth, *args)
61
+ end
62
+
63
+ define_method "#{meth}=" do |*args|
64
+ set_attribute(meth, *args)
65
+ end
66
+ end
67
+
68
+ def define_association_method(meth)
69
+ define_attribute_method meth
70
+ define_method meth do |*args|
71
+ get_association(meth, *args)
72
+ end
73
+
74
+ define_method "#{meth}=" do |*args|
75
+ set_association(meth, *args)
76
+ end
77
+ end
78
+
79
+ def define_x2m_ids_association_method(meth)
80
+ define_method "#{meth}_ids" do |*args|
81
+ @associations[meth]
82
+ end
83
+ end
84
+
85
+ def define_m2o_association_method(meth)
86
+ define_method "#{meth}_id" do |*args|
87
+ if @associations[meth].is_a? Array
88
+ @associations[meth][0]
89
+ else
90
+ r = get_association(meth, *args)
91
+ r.is_a?(Ooor::Base) ? r.id : r
92
+ end
93
+ end
94
+ end
95
+
96
+ def reload_field_definition(k, field)
97
+ case field['type']
98
+ when 'many2one'
99
+ many2one_associations[k] = field
100
+ when 'one2many'
101
+ one2many_associations[k] = field
102
+ when 'many2many'
103
+ many2many_associations[k] = field
104
+ when 'reference'
105
+ polymorphic_m2o_associations[k] = field
106
+ else
107
+ fields[k] = field if field['name'] != 'id'
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ attr_accessor :_display_name
114
+ alias _name _display_name
115
+
116
+ def _destroy=(dummy)
117
+ @marked_for_destruction = true unless dummy.blank? || ["false", "0", 0].index(dummy)
118
+ end
119
+
120
+ def _destroy
121
+ @marked_for_destruction
122
+ end
123
+
124
+ def lazy_load(meth, *args)
125
+ @lazy = false
126
+ fields = (self.class.fast_fields + [meth]).uniq
127
+ load(rpc_execute('read', [@attributes["id"]], fields, *args || context)[0]).tap do
128
+ @lazy = false
129
+ end
130
+ end
131
+
132
+ def get_attribute(meth, *args)
133
+ lazy_load(meth, *args) if @lazy && @attributes["id"] && !@attributes.has_key?(meth)
134
+ if @attributes.has_key?(meth)
135
+ @attributes[meth]
136
+ elsif @attributes["id"] # if field is computed for instance
137
+ @attributes[meth] = rpc_execute('read', [@attributes["id"]], [meth], *args || context)[0][meth]
138
+ else
139
+ nil
140
+ end
141
+ end
142
+
143
+ def set_attribute(meth, *args)
144
+ value = sanitize_attribute(meth, args[0])
145
+ @attributes[meth] ||= nil
146
+ send("#{meth}_will_change!") unless @attributes[meth] == value
147
+ @attributes[meth] = value
148
+ end
149
+
150
+ def get_association(meth, *args)
151
+ return @associations[meth] || :undef if @skip
152
+ lazy_load(meth, *args) if @lazy
153
+ if @loaded_associations.has_key?(meth)
154
+ @loaded_associations[meth]
155
+ elsif @associations.has_key?(meth)
156
+ @loaded_associations[meth] = relationnal_result(meth, *args)
157
+ else
158
+ if @attributes["id"]
159
+ @associations[meth] = rpc_execute('read', [@attributes["id"]], [meth], *args || context)[0][meth]
160
+ @loaded_associations[meth] = relationnal_result(meth, *args)
161
+ elsif self.class.one2many_associations.has_key?(meth) || self.class.many2many_associations.has_key?(meth)
162
+ load_x2m_association(meth, [], *args)
163
+ else
164
+ nil
165
+ end
166
+ end
167
+ end
168
+
169
+ def set_association(meth, *args)
170
+ value = sanitize_association(meth, args[0])
171
+ if self.class.many2one_associations.has_key?(meth) # TODO detect false positives changes for other associations too
172
+ if @associations[meth].is_a?(Array) && @associations[meth][0] == value \
173
+ || @associations[meth] == value #\
174
+ return value
175
+ end
176
+ end
177
+ @skip = true
178
+ send("#{meth}_will_change!")
179
+ @skip = false
180
+ if value.is_a?(Ooor::Base) || value.is_a?(Array) && !value.empty? && value.all? {|i| i.is_a?(Ooor::Base)}
181
+ @loaded_associations[meth] = value
182
+ else
183
+ @loaded_associations.delete(meth)
184
+ end
185
+ @associations[meth] = value
186
+ end
187
+
188
+ # # Raise NoMethodError if the named attribute does not exist in order to preserve behavior expected by #clone.
189
+ # def attribute(name)
190
+ # key = name.to_s
191
+ # if self.class.fields.has_key?(key) #TODO check not symbols
192
+ # get_attribute(key)
193
+ # elsif self.class.associations_keys.index(key)
194
+ # get_association(key)
195
+ # else
196
+ # raise NoMethodError
197
+ # end
198
+ # end
199
+
200
+ def method_missing(method_symbol, *arguments)
201
+ self.class.reload_fields_definition(false)
202
+ if id
203
+ rpc_execute(method_symbol, [id], *arguments) #we assume that's an action
204
+ else
205
+ super
206
+ end
207
+ rescue UnknownAttributeOrAssociationError => e
208
+ e.klass = self.class
209
+ raise e
210
+ end
211
+
212
+ end
213
+ end