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,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