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