Merge lp:~openerp-dev/openobject-server/trunk-apiculture-rco into lp:openobject-server

Proposed by Raphael Collet (OpenERP)
Status: Work in progress
Proposed branch: lp:~openerp-dev/openobject-server/trunk-apiculture-rco
Merge into: lp:openobject-server
Diff against target: 11970 lines (+6435/-2392)
92 files modified
doc/03_module_dev_02.rst (+1/-0)
doc/03_module_dev_03.rst (+13/-4)
doc/api_models.rst (+16/-2)
doc/index.rst (+4/-3)
doc/new_api.rst (+138/-0)
openerp/__init__.py (+15/-0)
openerp/addons/base/__openerp__.py (+1/-2)
openerp/addons/base/base.sql (+1/-36)
openerp/addons/base/base_menu.xml (+4/-0)
openerp/addons/base/ir/ir_actions.py (+6/-6)
openerp/addons/base/ir/ir_attachment.py (+2/-2)
openerp/addons/base/ir/ir_cron.py (+5/-4)
openerp/addons/base/ir/ir_mail_server.py (+3/-6)
openerp/addons/base/ir/ir_model.py (+38/-42)
openerp/addons/base/ir/ir_rule.py (+2/-2)
openerp/addons/base/ir/ir_sequence.py (+4/-3)
openerp/addons/base/ir/ir_translation.py (+3/-3)
openerp/addons/base/ir/ir_ui_menu.py (+18/-16)
openerp/addons/base/ir/ir_ui_view.py (+14/-8)
openerp/addons/base/ir/ir_values.py (+26/-1)
openerp/addons/base/module/module.py (+66/-45)
openerp/addons/base/res/ir_property.py (+3/-4)
openerp/addons/base/res/res_company.py (+2/-1)
openerp/addons/base/res/res_config.py (+2/-2)
openerp/addons/base/res/res_currency.py (+11/-18)
openerp/addons/base/res/res_partner.py (+147/-143)
openerp/addons/base/res/res_users.py (+13/-13)
openerp/addons/base/security/base_security.xml (+0/-5)
openerp/addons/base/test/base_test.yml (+1/-1)
openerp/addons/base/test/test_ir_rule.yml (+1/-1)
openerp/addons/base/test/test_osv_expression.yml (+2/-2)
openerp/cli/server.py (+1/-0)
openerp/exceptions.py (+15/-1)
openerp/modules/loading.py (+2/-0)
openerp/modules/module.py (+18/-16)
openerp/modules/registry.py (+34/-14)
openerp/netsvc.py (+2/-6)
openerp/osv/__init__.py (+3/-2)
openerp/osv/api.py (+760/-0)
openerp/osv/expression.py (+125/-105)
openerp/osv/fields.py (+138/-110)
openerp/osv/fields2.py (+995/-0)
openerp/osv/orm.py (+1679/-1348)
openerp/osv/scope.py (+380/-0)
openerp/report/custom.py (+5/-6)
openerp/report/print_xml.py (+12/-30)
openerp/report/report_sxw.py (+18/-87)
openerp/service/model.py (+1/-1)
openerp/service/security.py (+1/-1)
openerp/tests/__init__.py (+3/-3)
openerp/tests/addons/test_impex/models.py (+55/-39)
openerp/tests/addons/test_impex/tests/test_export.py (+8/-11)
openerp/tests/addons/test_impex/tests/test_import.py (+5/-5)
openerp/tests/addons/test_impex/tests/test_load.py (+6/-6)
openerp/tests/addons/test_inherit/__init__.py (+3/-0)
openerp/tests/addons/test_inherit/__openerp__.py (+15/-0)
openerp/tests/addons/test_inherit/ir.model.access.csv (+1/-0)
openerp/tests/addons/test_inherit/models.py (+29/-0)
openerp/tests/addons/test_inherit/tests/__init__.py (+12/-0)
openerp/tests/addons/test_inherit/tests/test_inherit.py (+17/-0)
openerp/tests/addons/test_new_api/__init__.py (+2/-0)
openerp/tests/addons/test_new_api/__openerp__.py (+15/-0)
openerp/tests/addons/test_new_api/demo_data.xml (+14/-0)
openerp/tests/addons/test_new_api/models.py (+196/-0)
openerp/tests/addons/test_new_api/tests/__init__.py (+18/-0)
openerp/tests/addons/test_new_api/tests/test_attributes.py (+25/-0)
openerp/tests/addons/test_new_api/tests/test_field_conversions.py (+11/-0)
openerp/tests/addons/test_new_api/tests/test_new_fields.py (+397/-0)
openerp/tests/addons/test_new_api/tests/test_onchange.py (+46/-0)
openerp/tests/addons/test_new_api/tests/test_related.py (+3/-58)
openerp/tests/addons/test_new_api_extend/__init__.py (+3/-0)
openerp/tests/addons/test_new_api_extend/__openerp__.py (+14/-0)
openerp/tests/addons/test_new_api_extend/models.py (+12/-0)
openerp/tests/addons/test_new_api_extend/tests/__init__.py (+8/-0)
openerp/tests/addons/test_new_api_extend/tests/extend_class.py (+26/-0)
openerp/tests/addons/test_workflow/models.py (+8/-7)
openerp/tests/addons/test_workflow/tests/test_workflow.py (+1/-1)
openerp/tests/common.py (+7/-2)
openerp/tests/test_acl.py (+34/-20)
openerp/tests/test_api.py (+462/-0)
openerp/tests/test_orm.py (+14/-0)
openerp/tools/__init__.py (+1/-1)
openerp/tools/cache.py (+86/-86)
openerp/tools/convert.py (+3/-1)
openerp/tools/func.py (+29/-1)
openerp/tools/misc.py (+7/-0)
openerp/tools/test_reports.py (+1/-1)
openerp/tools/translate.py (+46/-41)
openerp/tools/yaml_import.py (+3/-1)
openerp/workflow/workitem.py (+3/-4)
openerpcommand/read.py (+1/-1)
setup.py (+34/-1)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-apiculture-rco
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+157040@code.launchpad.net

Description of the change

Task: RD Framework / new api: ORM
 - encapsulate session data (cr, uid, context) into a separate object, called a "scope"
 - scopes are nestable, and introduced with the statement "with" => scope is global and implicit
 - introduce "record", "recordset" and "null" as instances of BaseModel => methods available on all instances
 - removed former classes browse_record, browse_record_list and browse_null
 - attach record cache to scopes => automatic cache sharing
 - add cache invalidation => cache consistency
 - introduce method decorators to define record, recordset or model methods => real object style method API
 - in model class, use docstring as default _description, and class name as default _name

To post a comment you must log in.
4892. By Raphael Collet (OpenERP)

[IMP] orm: improve str representation of model instances

4893. By Raphael Collet (OpenERP)

[IMP] orm: support the creation of non-scoped records

* By default, method browse(ids) returns scoped records (backwards compatible
  behavior), while browse(ids, scoped=False) returns non-scoped records.
* The decorator @returns generates non-scoped records.

4894. By Raphael Collet (OpenERP)

[IMP] ir.model.access: automatically invalidate the record caches when access rights are modified

4895. By Raphael Collet (OpenERP)

[IMP] test_api: improve tests on scopes

4896. By Raphael Collet (OpenERP)

[IMP] orm: better error handling in new code

4897. By Raphael Collet (OpenERP)

[IMP] orm: when converting exception, attach existing traceback

4898. By Raphael Collet (OpenERP)

[IMP] api: new record-map convention: map results into a list instead of a dict

4899. By Raphael Collet (OpenERP)

[FIX] test_impex: change calls to name_get() to follow new record-map convention

4900. By Raphael Collet (OpenERP)

[FIX] workflow: when evaluating a trigger expression, handle the case of a single output

4901. By Raphael Collet (OpenERP)

[MERGE] from trunk

4902. By Raphael Collet (OpenERP)

[IMP] orm tests: move test on related fields in openerp/tests/addons/test_fields

4903. By Raphael Collet (OpenERP)

[IMP] openerp.osv.fields2: new module with new-style fields

4904. By Raphael Collet (OpenERP)

[IMP] new fields: record.X and record['X'] are now equivalent

4905. By Raphael Collet (OpenERP)

[IMP] orm: method read() now handles new-style fields
 - existing method read() has been renamed _read();
 - method _read() takes care of old-style fields only;
 - new higher-level read() handles both field styles.

4906. By Raphael Collet (OpenERP)

[IMP] orm: add automatic recomputation of new-style function fields

4907. By Raphael Collet (OpenERP)

[IMP] orm: improve methods invalidate_cache()

4908. By Raphael Collet (OpenERP)

[IMP] orm tests: improve test on new-style fields

4909. By Raphael Collet (OpenERP)

[IMP] new fields: improve field recomputation, and handle stored fields as well
 - redesign the recomputation algorithm such that it handles field dependencies
 - use it to compute fields for existing records when column is created in db

4910. By Raphael Collet (OpenERP)

[FIX] orm: invalidate the whole cache upon record delete, because of ondelete='cascade', etc.

4911. By Raphael Collet (OpenERP)

[IMP] orm: improve names of method that implement record.field, and check for deleted records

4912. By Raphael Collet (OpenERP)

[IMP] new fields: add test for a regular field (no compute method)

4913. By Raphael Collet (OpenERP)

[IMP] orm: internally recordsets use a tuple of ids instead of a list
 - this is only internal stuff, and does not break anything
 - this enforces the idea that a recordset is an immutable collection

4914. By Raphael Collet (OpenERP)

[IMP] new fields: small improvement when deriving string attribute

4915. By Raphael Collet (OpenERP)

[IMP] new fields: add test for compute method defined on recordset

4916. By Raphael Collet (OpenERP)

[MERGE] from trunk

4917. By Raphael Collet (OpenERP)

[FIX] new fields: in recompute_spec(), avoid returning useless specs (empty ids)

4918. By Raphael Collet (OpenERP)

[FIX] orm: rewrite method read() to always check access rights before doing anything else

4919. By Raphael Collet (OpenERP)

[IMP] orm: make method is_null() more robust

4920. By Raphael Collet (OpenERP)

[IMP] new fields: add method _set_field() in BaseModel

4921. By Raphael Collet (OpenERP)

[IMP] new fields: improve field copy, and fix check for non-attribute fields

4922. By Raphael Collet (OpenERP)

[IMP] new fields: add new-style fields to interface all columns

4923. By Raphael Collet (OpenERP)

[ADD] new fields: add boolean and float fields

4924. By Raphael Collet (OpenERP)

[ADD] new fields: add char, text, html, date and datetime fields

4925. By Raphael Collet (OpenERP)

[IMP] new fields: refactor methods from_column and to_column

4926. By Raphael Collet (OpenERP)

[IMP] new fields: check value format when assigning a field

4927. By Raphael Collet (OpenERP)

[ADD] new fields: add binary and selection fields

4928. By Raphael Collet (OpenERP)

[IMP] new fields: small refactoring of field methods

4929. By Raphael Collet (OpenERP)

[IMP] new fields: add missing option 'translate' on string fields

4930. By Raphael Collet (OpenERP)

[IMP] orm: make null instances a subset of records

4931. By Raphael Collet (OpenERP)

[IMP] new fields: fix recomputation (spec stored in thread-local instead of scope proxy), and improve tests

4932. By Raphael Collet (OpenERP)

[ADD] new fields: add relational fields (many2one, one2many, many2many)

4933. By Raphael Collet (OpenERP)

[ADD] new fields: add reference fields + fix small error in selection fields

4934. By Raphael Collet (OpenERP)

[MERGE] from trunk

4935. By Raphael Collet (OpenERP)

[IMP] orm: browse() always returns instances attached to a scope

4936. By Raphael Collet (OpenERP)

[IMP] orm: refactor record and recordset instances creation
 - add explicit conversion method to_record(), to_recordset()
 - null(), record(), recordset() return instances attached to the current scope
 - consequently, all BaseModel instances (except the registry one) are attached
   to a scope
 - adapt existing code using those methods

4937. By Raphael Collet (OpenERP)

[IMP] orm: recordset implementation now refers to records instead of ids

4938. By Raphael Collet (OpenERP)

[IMP] openerp.osv.scope: improve api of scope proxy

4939. By Raphael Collet (OpenERP)

[IMP] orm: method recordset() called without params returns an empty recordset

4940. By Raphael Collet (OpenERP)

[MERGE] from trunk

4941. By Raphael Collet (OpenERP)

[IMP] orm: remove instance property scope, and add method to attach instances to the current scope

4942. By Raphael Collet (OpenERP)

[IMP] orm: in the record cache, store values at the record level

4943. By Raphael Collet (OpenERP)

[IMP] orm, scope: change the implementation of the record cache
 - the cache is a dictionary like {model_name: {record_id: record, ...}, ...}
 - each record holds its own cache: record._data = {field_name: value, ...}
 - the code to get/set fields on records has been adapted

4944. By Raphael Collet (OpenERP)

[FIX] new fields: attach model instances to the same scope as a record's cache

4945. By Raphael Collet (OpenERP)

[IMP] new fields: improve test methods

4946. By Raphael Collet (OpenERP)

[IMP] orm: small code improvements

4947. By Raphael Collet (OpenERP)

[IMP] orm: avoid methods record() and recordset() looking up for records in the cache

4948. By Raphael Collet (OpenERP)

[IMP] orm: make method to_record() more tolerant, and method one() to be strict

4949. By Raphael Collet (OpenERP)

[FIX] orm: when assigning record field with setattr, call write() in record's scope

4950. By Raphael Collet (OpenERP)

[IMP] orm: interface all _columns with new-style fields

4951. By Raphael Collet (OpenERP)

[FIX] new fields tests: improve selection of partners when assigning parents!

4952. By Raphael Collet (OpenERP)

[FIX] reports: fix method setLang() so that it 're-scopes' the objects in a clean way

4953. By Raphael Collet (OpenERP)

[IMP] orm: rename instance attributes (_id -> _record_id, _data -> _record_cache)

4954. By Raphael Collet (OpenERP)

[IMP] orm: improve error handling in method _get_field()

4955. By Raphael Collet (OpenERP)

[IMP] orm, new fields: small code cleanup

4956. By Raphael Collet (OpenERP)

[IMP] orm: introduce draft records

4957. By Raphael Collet (OpenERP)

[IMP] orm: add pseudo-class Draft for draft records

4958. By Raphael Collet (OpenERP)

[IMP] fields: make method read_to_cache() more robust

4959. By Raphael Collet (OpenERP)

[IMP] orm: rewrite method default_get() in order to simplify refactoring with draft records

4960. By Raphael Collet (OpenERP)

[IMP] orm: small improvements for draft records, and add __setitem__ method

4961. By Raphael Collet (OpenERP)

[FIX] new fields: forgot method record_to_read() on one2many and many2many fields

4962. By Raphael Collet (OpenERP)

[ADD] new fields: add related fields (non-stored ones)

4963. By Raphael Collet (OpenERP)

[IMP] orm: interface inherited fields with new-style related fields
+fix the field 'date' in res.currency to avoid cache inconsistencies

4964. By Raphael Collet (OpenERP)

[FIX] scope cache: never remove records from the cache, as it breaks an implementation property

This fixes a subtle issue that may happen after a cache invalidation. If you
remove a record from the scope cache, you may still refer and read that record,
but the values read from the database will be stored in another record instance!

In fact this issue did not cause a bug, because the record was "forced" into the
cache before fetching data from the database. But the "prefetching" effect (read
all records of the same model in the cache) was broken if you try to read other
records that were also removed from the cache!

4965. By Raphael Collet (OpenERP)

[IMP] scope cache: fix the consistency check of the cache (because of the modified behavior of read())

4966. By Raphael Collet (OpenERP)

[IMP] orm: code simplification, using the fact that all fields are interfaced with new-style fields

4967. By Raphael Collet (OpenERP)

[IMP] fields2: determine inverse fields, and improve cache invalidation and recomputation

Revision history for this message
Xavier (Open ERP) (xmo-deactivatedaccount) wrote :

one note during reading, wouldn't the `decorator.decorator(self.lookup, method)` call in cache.py work just as well as `functools.partial(self.lookup, method)`?

4968. By Raphael Collet (OpenERP)

[IMP] orm: put all conversion method from/to cache in new fields classes

4969. By Raphael Collet (OpenERP)

[IMP] orm: change implementation of draft records, add them a specific cache

4970. By Raphael Collet (OpenERP)

[IMP] orm: reimplement default_get with draft records

4971. By Raphael Collet (OpenERP)

[IMP] orm: optimize method add_default_value() by caching defaults from ir_values

4972. By Xavier (Open ERP)

[IMP] rename new API test module for clarity

4973. By Xavier (Open ERP)

[ADD] bunch of todos in doc (for both implementation and documentation of new API)

4974. By Xavier (Open ERP)

[ADD] start of onchange implementation

* fix _get_field for non-stored fields on draft records: computed
  fields will be stored in _record_draft, not _record_cache

* add handling of cache invalidation to draft fields: when setting a
  value on a field, invalidate the stuff which depends on it (nb: not
  currently recursive)

The current contract for Model.onchange is simple: give it the field
change which triggered the call and all values[0], onchange will build
a draft record with all values *but* the trigger in its cache (as if
they were defaults or had been read from the db) then set the changed
field via the normal UI.

Then iterate all fields and see what changed from the values provided,
this should yield only the fields which 1. were recomputed 2. to a
value different than the one they previously had.

[0] this may be a very, very bad idea for huge & complex objects full
    of o2ms & m2ms

4975. By Xavier (Open ERP)

[DOC] more todos

4976. By Xavier (Open ERP)

[FIX] forgot to add onchange tests

4977. By Xavier (Open ERP)

[FIX] apparently we still need to manually mark methods as api.record

4978. By Xavier (Open ERP)

[FIX] log_access fields

4979. By Xavier (Open ERP)

[REM] _protected attribute has been unused since revision 921 (2008)...

4980. By Xavier (Open ERP)

[FIX] m2o fields and conversion to columns

* Fix handling of ondelete when converting to a column

* Allow Field -> Column overwriting when the field was created
  explicitly (not field.interface), otherwise when overwriting a field
  in an _inherit the corresponding Column object is outdated and
  incorrect.

4981. By Xavier (Open ERP)

[FIX] correctly get default value from new-style fields when adding required attribute/constraint

4982. By Xavier (Open ERP)

[IMP] un-generalize getattr: since all _columns are also set up as fields, it's only a source of errors

in case of e.g. method name typos it'll go into _get_field then blow up

4983. By Xavier (Open ERP)

[FIX] fields_view_get for purely computed new-style fields

* generate a function column if store=False

* handle all setting on old-style (base and generated) fields

* slighly improve Field.to_column flow

* bypass db write on setattr if store=False

* remove more special handling of concurrency field (it's now a basic
  Field@store=False)

4984. By Vo Minh Thu

[IMP] fields2: add domain attribute to x2x.

4985. By Raphael Collet (OpenERP)

[MERGE] orm: store model attributes shared by all instance in the class instead of the instances

4986. By Raphael Collet (OpenERP)

[REF] orm: refactor the addition of a new-style field on a model's class

4987. By Raphael Collet (OpenERP)

[IMP] orm: turn some internal methods into class methods, and make them private

4988. By Raphael Collet (OpenERP)

[REF] orm: introduce 'id' as an explicit field (with special getter)

4989. By Xavier (Open ERP)

[IMP] enable debug mode post_mortem on CLI use of the server, not just rpc

4990. By Xavier (Open ERP)

[FIX] generalized symbol_* handling on function fields

4991. By Xavier (Open ERP)

[IMP] default values for ir_model datetime fields: use fields.datetime.now

4992. By Xavier (Open ERP)

[IMP] query building in create: string concatenation is 'orrible

4993. By Xavier (Open ERP)

[IMP] extract addition of defaults to a column so they stay in sync between new column and old-column-becoming-required

4994. By Xavier (Open ERP)

[IMP] creation of access fields & handling of concurrency computation methods, ensure access rights fields can't be written by a user

4995. By Xavier (Open ERP)

[IMP] remove creation of ir.ui.menu table in SQL, move security stuff around to get it to work correctly

4996. By Xavier (Open ERP)

[REM] useless creation of workflow tables in raw SQL

4997. By Raphael Collet (OpenERP)

[IMP] orm: reimplement fields_get with new-style fields

4998. By Raphael Collet (OpenERP)

[IMP] new fields: do not create columns for non-stored fields

4999. By Raphael Collet (OpenERP)

[FIX] orm: duplicate model attributes like _columns to avoid sharing their default value!

5000. By Xavier (Open ERP)

[IMP] use unittest2.TestCase.addCleanup for rollback global alteration to registry/fields in tests

5001. By Raphael Collet (OpenERP)

[IMP] scope: change the cache structure (to ease upcoming changes)

5002. By Raphael Collet (OpenERP)

[IMP] orm: in draft records, use a sentinel for a missing value

5003. By Raphael Collet (OpenERP)

[IMP] orm: remove attribute _record_cache from record instances

5004. By Raphael Collet (OpenERP)

[IMP] ir_model: introduce right scope when creating instance in registry

5005. By Raphael Collet (OpenERP)

[IMP] api: improve internal code, and rename decorators: @recordset -> @multi, @record -> @one

5006. By Raphael Collet (OpenERP)

[IMP] api: improve code for extracting context from arguments

5007. By Raphael Collet (OpenERP)

[IMP] orm: NEW DESIGN - now all model instances are recordsets

5008. By Raphael Collet (OpenERP)

[FIX] service: fix test on model returned from registry

5009. By Raphael Collet (OpenERP)

[MERGE] from trunk

5010. By Raphael Collet (OpenERP)

[REV] revid:<email address hidden>
this changeset was not correct:
 - column wkf_instance.uid still used in openerp/workflow/instance.py
 - table wkf_witm_trans still used in openerp/workflow/workitem.py
one has to clean up the code first...

5011. By Raphael Collet (OpenERP)

[FIX] test_impex: fix exports by reimplementing __export_row()

5012. By Raphael Collet (OpenERP)

[MERGE] from trunk

5013. By Raphael Collet (OpenERP)

[IMP] scope: do not use class Scope in non-ORM code

5014. By Raphael Collet (OpenERP)

[IMP] openerp: improve imports for module code

5015. By Raphael Collet (OpenERP)

[REM] orm: remove pseudo-classes Recordset, Record, Null

5016. By Raphael Collet (OpenERP)

[IMP] cache invalidation: simplify the 'spec' parameter

5017. By Raphael Collet (OpenERP)

[IMP] field recomputation: do not search for records in the case of non-stored fields

5018. By Raphael Collet (OpenERP)

[FIX] scope: handle the case of calls like scope(cr, uid, None)

5019. By Raphael Collet (OpenERP)

[IMP] orm: new implementation of draft records that fits better with the design

5020. By Raphael Collet (OpenERP)

[MERGE] from trunk

5021. By Raphael Collet (OpenERP)

[IMP] orm: reimplement new-style method onchange

5022. By Raphael Collet (OpenERP)

[ADD] orm: union, intersection, difference of recordset + set comparison

5023. By Raphael Collet (OpenERP)

[IMP] orm: test error cases for set operations

5024. By Raphael Collet (OpenERP)

[IMP] orm: improve record cache management
 - better prefetching: browse() introduces cache entries for its records
 - quicker access: memoize access to the cache in model instances

5025. By Raphael Collet (OpenERP)

[IMP] fields attribute cleanup: f.model, f.comodel and f.inverse_field are the objects instead of their names

5026. By Raphael Collet (OpenERP)

[IMP] scope: improve the cache invalidation API

5027. By Raphael Collet (OpenERP)

[REF] orm: improve management of recomputation of stored function fields
 - make fields manage their dependencies and recomputation triggers
 - introduce a global recomputation manager object (simpler than "specs")

5028. By Raphael Collet (OpenERP)

[IMP] scope: simplification of API and implementation
 - scopes are no longer iterable, use scope.args to retrieve cr, uid, context
 - use named args when calling a scope: scope(user=42), scope(context={...})

5029. By Raphael Collet (OpenERP)

[MERGE] from trunk

5030. By Raphael Collet (OpenERP)

[IMP] orm, fields: small code improvements (avoid __class__ and such)

5031. By Raphael Collet (OpenERP)

[IMP] new fields: when searching for records to recompute, make sure to retrieve all of them

5032. By Raphael Collet (OpenERP)

[IMP] orm: add automatic cache invalidation for non-stored fields.function

5033. By Raphael Collet (OpenERP)

[IMP] orm: delegate compute/read fields on fields themselves

5034. By Raphael Collet (OpenERP)

[IMP] orm: change API of draft records

5035. By Raphael Collet (OpenERP)

[REF] orm: improve the representation of model instances
 - each instance refers directly to its records' cache dictionaries
 - the 'id' of each record is in its cache dictionary (except new records)
 - VirtualID is no longer needed
 - the access to a record's cache is faster

5036. By Raphael Collet (OpenERP)

[IMP] record cache: better implementation for cache invalidation
 - the record cache is no longer a built-in dict
 - the model cache keeps track of which fields are present in record caches
 - invalidating an already-invalidated field simply does nothing

5037. By Raphael Collet (OpenERP)

[IMP] new fields: when dealing with modified fields, invalidate the cache in a more direct way

5038. By Raphael Collet (OpenERP)

[IMP] scope: improve code for checking the cache

5039. By Raphael Collet (OpenERP)

[IMP] new fields: add helper function to provide a constant default for a field

5040. By Raphael Collet (OpenERP)

[IMP] orm: separate concepts of 'new' and 'draft' instances

5041. By Raphael Collet (OpenERP)

[IMP] orm: fix the mess with 'id' in record cache

5042. By Raphael Collet (OpenERP)

[IMP] scope: use better exception type when checking the cache

5043. By Raphael Collet (OpenERP)

[IMP] fields: when computing the value of a field, do it for all records in cache

5044. By Raphael Collet (OpenERP)

[IMP] orm: improve the recomputation manager to hide some hack

5045. By Raphael Collet (OpenERP)

[IMP] record cache: small code cleanup

5046. By Raphael Collet (OpenERP)

[FIX] orm: fix instance comparison, which was not independent from scope!

5047. By Raphael Collet (OpenERP)

[IMP] record cache: add special values to missing default values, fields being computed, etc.

5048. By Raphael Collet (OpenERP)

[IMP] new fields: make date/datetime conversions more robust

5049. By Raphael Collet (OpenERP)

[FIX] orm: fix computation of field __last_update

5050. By Raphael Collet (OpenERP)

[FIX] new fields: add missing method redirection in related fields

5051. By Raphael Collet (OpenERP)

[IMP] new fields: refactor conversion methods to the cache level

5052. By Raphael Collet (OpenERP)

[IMP] orm: small code cleanup

5053. By Raphael Collet (OpenERP)

[ADD] new fields: add inverse computation of fields

5054. By Raphael Collet (OpenERP)

[ADD] osv/cache: make the record cache read/write-through

5055. By Raphael Collet (OpenERP)

[IMP] record cache: improve implementation, use named tuples for cache slots

5056. By Raphael Collet (OpenERP)

[IMP] osv cache: improve documentation

5057. By Raphael Collet (OpenERP)

[MERGE] from trunk

5058. By Raphael Collet (OpenERP)

[ADD] new fields: add search on fields
 - add an option to rewrite a condition on a field
 - in expression.py, rename local variables to fit new terminology

5059. By Raphael Collet (OpenERP)

[FIX] test_impex: make code consistent with values in selection function (ints not strings)

5060. By Raphael Collet (OpenERP)

[FIX] test_workflow: use 'class' instead of type() to create model classes,
because type() registers the class under the wrong module name (openerp.osv.api)

5061. By Raphael Collet (OpenERP)

[IMP] api: improve documentation of attributes set on methods

5062. By Raphael Collet (OpenERP)

[ADD] orm: add new-style Python constraints with a decorator

5063. By Raphael Collet (OpenERP)

[IMP] orm: fix and extend a bit the generic onchange method

5064. By Raphael Collet (OpenERP)

[FIX] test_new_api: make test pass when test_new_api_extend is installed

5065. By Raphael Collet (OpenERP)

[IMP] new fields: reimplement related fields as compute fields

5066. By Raphael Collet (OpenERP)

[IMP] new fields: when setting up a related field, complete the setup of a field before getting its attributes

5067. By Raphael Collet (OpenERP)

[MERGE] from trunk

5068. By Raphael Collet (OpenERP)

[FIX] registry: buggy non-zero test on model

5069. By Raphael Collet (OpenERP)

[IMP] orm: make result of onchange() backwards-compatible

5070. By Raphael Collet (OpenERP)

[IMP] fields2: reorganize code, and ensure that _ready is reset upon copy

5071. By Raphael Collet (OpenERP)

[IMP] base/ir.module.module: optimize state_update() (minimize calls to write)

5072. By Raphael Collet (OpenERP)

[FIX] new fields: on related selection fields, the selection must be computed by the target field

5073. By Raphael Collet (OpenERP)

[MERGE] from trunk

5074. By Raphael Collet (OpenERP)

[IMP] fields2: remove pudb invocation

5075. By Raphael Collet (OpenERP)

[FIX] scope: make scope() more robust when None is given as context

5076. By Raphael Collet (OpenERP)

[FIX] orm: introduce scope explicitly in method scoped()

5077. By Raphael Collet (OpenERP)

[FIX] fields2: bypass access rights to compute related fields

5078. By Raphael Collet (OpenERP)

[FIX] fields2: catch exceptions when computing related fields

5079. By Raphael Collet (OpenERP)

[FIX] orm: apparently, some code in mail needs explicit cache invalidation

5080. By Raphael Collet (OpenERP)

[IMP] cache: simplify implementation of cache slots

5081. By Raphael Collet (OpenERP)

[FIX] orm, registry: add hooks on models before and after a registry update
This fixes a bug where some new fields were keeping a reference to a model
that has been updated in the registry (Many2one's comodel, for instance).

5082. By Raphael Collet (OpenERP)

[FIX] new fields: in *2many values, one may have a dict in the list of commands

5083. By Raphael Collet (OpenERP)

[IMP] fields2: add attributes 'states' to new fields

5084. By Raphael Collet (OpenERP)

[MERGE] from trunk

5085. By Raphael Collet (OpenERP)

[IMP] orm: make read(), create() and write() tolerate unknown fields (with warning)

5086. By Raphael Collet (OpenERP)

[IMP] orm: improve warning message when read/create/write with unknown fields

5087. By Raphael Collet (OpenERP)

[MERGE] from trunk

5088. By Raphael Collet (OpenERP)

[IMP] new fields: in case a field depends on '*', it should not depend on itself

5089. By Raphael Collet (OpenERP)

[IMP] new fields: small code reorganization

5090. By Raphael Collet (OpenERP)

[IMP] orm: add field display_name, and reimplement name_{get,create,search} in terms of it

5091. By Raphael Collet (OpenERP)

[ADD] fields2: add option delegate=True in many2one fields to include field in dict _inherits

5092. By Raphael Collet (OpenERP)

[IMP] orm: small simplification in method _set_magic_fields()

5093. By Raphael Collet (OpenERP)

[MERGE] trunk-apiculture-snakecase-rco: use snake-casing for model names

5094. By Raphael Collet (OpenERP)

[FIX] fields: fix _symbol_set for fields.model

5095. By Raphael Collet (OpenERP)

[IMP] fields2: do not compute fields on non-existing records
 - This reduces the need to handle exceptions when computing fields.

5096. By Raphael Collet (OpenERP)

[IMP] orm: make inverse and search on display_name more robust

5097. By Raphael Collet (OpenERP)

[FIX] base: in ir_module_module, snakecase model names explicitly

5098. By Raphael Collet (OpenERP)

[MERGE] from trunk

5099. By Raphael Collet (OpenERP)

[FIX] openerp.tools: make snake_dict raise KeyError when looking up with a non-string

5100. By Raphael Collet (OpenERP)

[IMP] api: reduce methods wrappers to a single layer; this eases debugging

5101. By Raphael Collet (OpenERP)

[MERGE] from trunk

5102. By Raphael Collet (OpenERP)

[FIX] test_workflow: remove unexpected argument that is mistakenly interpreted as a context

5103. By Raphael Collet (OpenERP)

[FIX] orm: avoid doing None + list in name_search()

5104. By Raphael Collet (OpenERP)

[FIX] ir_ui_view: snake-case model name in sql query

5105. By Raphael Collet (OpenERP)

[IMP] fields2: small code cleanup

5106. By Raphael Collet (OpenERP)

[IMP] openerp/osv/*: remove unused imports

5107. By Raphael Collet (OpenERP)

[FIX] orm: make resolve_2many_commands() robust when argument is False

5108. By Raphael Collet (OpenERP)

[FIX] orm, fields2: make field 'id' non-overridable
The implementation fields2.Id is critical to make "record.id" work properly.

5109. By Raphael Collet (OpenERP)

[MERGE] from trunk

5110. By Raphael Collet (OpenERP)

[IMP] fields2: cleanup of Field.interface

5111. By Raphael Collet (OpenERP)

[IMP] fields2: improve code about digits in field Float

5112. By Raphael Collet (OpenERP)

[IMP] fields, fields2: cleanup of code converting one to another

5113. By Raphael Collet (OpenERP)

[IMP] fields2: uniformize internal API to export field attributes

5114. By Raphael Collet (OpenERP)

[FIX] orm: in _add_missing_default_values, do not ask default values for magic fields

5115. By Raphael Collet (OpenERP)

[IMP] openerp/tools: optimize lazy_property.reset_all()

5116. By Raphael Collet (OpenERP)

[REVERT] snake-casing of model names: not worth the bugs and performance penalty

5117. By Raphael Collet (OpenERP)

[MERGE] trunk-apiculture-stw: make logs from mail server less noisy

5118. By Stephane Wirtel (OpenERP)

[FIX][TMP] Use a fields.char instead of the fields.function(selection) for the ir.ui.view#type field

5119. By Stephane Wirtel (OpenERP)

[FIX] Authorize the modules to define new kind of views.

5120. By Raphael Collet (OpenERP)

[IMP] ir_ui_view: improve method name

5121. By Stephane Wirtel (OpenERP)

[MERGE] trunk-apiculture-recname: avoid non-null constraint if _rec_name is required

5122. By Raphael Collet (OpenERP)

[FIX] ir_ui_menu: put the menu cache on the model's class, instead of an instance

5123. By Stephane Wirtel (OpenERP)

[FIX] In the ORM, the functions who start with 'set_' are used by the
default_get method. But it's not the case for set_default_value_on_column.
We rename this method to avoid this mistake.

5124. By Raphael Collet (OpenERP)

[FIX] ir_model_data: put the cache dictionary 'loads' on the class instead of an instance

5125. By Raphael Collet (OpenERP)

[IMP] orm: refactor method create_instance()

5126. By Raphael Collet (OpenERP)

[IMP] orm: remove method __getattr__ from BaseModel; it is bug-prone and not really useful

5127. By Raphael Collet (OpenERP)

[IMP] scope: small code refactoring

5128. By Raphael Collet (OpenERP)

[IMP] ir_sequence: small code cleanup to help API converters

5129. By Raphael Collet (OpenERP)

[IMP] orm, scope: move the 'draft' flag on the scope instead of the records

5130. By Raphael Collet (OpenERP)

[IMP] scope: draft mode is now global to all scopes

5131. By Stephane Wirtel (OpenERP)

[FIX] openerp/report: use_global_header is a transient field on the ir.action.report.xml object. We use it to avoid a change in the API.

5132. By Stephane Wirtel (OpenERP)

[FIX] test_mail: Rewrite the XSS test

5133. By Raphael Collet (OpenERP)

[IMP] orm: simplify the records cache (remove slots)

5134. By Raphael Collet (OpenERP)

[IMP] api, fields: @depends can now be given a function to compute dependencies
For field 'display_name', this optimizes triggers, since dependencies were given
as '*', which adds a trigger on *all* fields of the model!

5135. By Raphael Collet (OpenERP)

[MERGE] from trunk

5136. By Raphael Collet (OpenERP)

[MERGE] from trunk

5137. By Raphael Collet (OpenERP)

[FIX] orm: insert the instance in the registry sooner (fixes missing fields in _inherits_reload())

5138. By Stephane Wirtel (OpenERP)

[IMP] Add a new test for the inheritance computing.

5139. By Stephane Wirtel (OpenERP)

[IMP] Add a test for the new api. This test will create a new model and checks
that we can add a new attribute on the instance of a model.

Sometimes, by example, in the report engine, we add a new attribute on the
instance. By the way, we can add this attribute without any write to the
database.

5140. By Stephane Wirtel (OpenERP)

[FIX] Add a new attribue into the instance of the ir.actions.report.xml instance
because we need of a flag to use or not the right header in the report engine.

The previous implementation was wrong! Sorry

5141. By Stephane Wirtel (OpenERP)

[REF] Rewrite the fields_get method of the ORM. Especially the part for the
translation. This new implementation is just a refactoring and does not change
the behavior the function.

5142. By Raphael Collet (OpenERP)

[IMP] orm: improve __sub__ and __and__ on records, avoid comparing dict contents

5143. By Raphael Collet (OpenERP)

[IMP] base: always use method read() with multiple ids (this is necessary to change its API)

5144. By Raphael Collet (OpenERP)

[FIX] res_company: fix call to read()

5145. By Raphael Collet (OpenERP)

[IMP] new fields: decorator @one enables recursive computation of fields

5146. By Raphael Collet (OpenERP)

[IMP] new fields: enable recursive computation of stored fields, too

5147. By Raphael Collet (OpenERP)

[IMP] orm: reimplement method read() using the new API
 - call chain:
    record.read() uses new API to get and convert values
    -> field.__get__() retrieves value from cache or compute it
    -> field.determine_value() determines whether to compute or read value
    -> record._prefetch_field() determines which records and fields to read
    -> record._read_into_cache() actually reads data and store it in cache
 - exceptions are stored as values in cache, and raised upon reading cache
 - added a new exception for missing records
 - ISSUE: we cannot reproduce the behavior of read() with a single id; it now
   returns a list of dicts instead of a dict

5148. By Stephane Wirtel (OpenERP)

[FIX] base: The default value for the name of a currency rate is now a
field.datetime, and thus in this case, we have to use the default value for this
kind of a field.

5149. By Stephane Wirtel (OpenERP)

[FIX] openerp/osv/orm: Check if the fields parameter of the read method is not
None or False. In this case, the method will use the _fields attribute of the
current object. Otherwise, this code will crash because a boolean is not
iterable.

5150. By Raphael Collet (OpenERP)

[IMP] scope: optimize API of scope.invalidate(), resulting in 5% to 10% speedup
 - every call to proxy.invalidate iterates over all scopes (which is costly to access)
 - the new api groups many calls into a single one, which leads to less access to all_scopes

5151. By Raphael Collet (OpenERP)

[IMP] orm: rename _read_cache into _get_cache to make it less confusing

5152. By Raphael Collet (OpenERP)

[IMP] orm: change structure of the cache, resulting in 5-10% speedup
 - new cache is indexed by field object, then by record id
 - cache access is a bit slower, but invalidation is way faster

5153. By Raphael Collet (OpenERP)

[IMP] orm: private method _update_cache now updates all records in self

5154. By Raphael Collet (OpenERP)

[REF] fields2: generalize FailedValues to special values stored in the cache
 - SpecialValue encapsulates null values in new records missing default values
 - FailedValue encapsulates exceptions for various kinds of failures to report
 - These special values transmit information from the point where a field value
   is determined to the point where the cache is actually read.

5155. By Raphael Collet (OpenERP)

[FIX] fields2: undefined variable

5156. By Raphael Collet (OpenERP)

[FIX] base: missing dependency on computed field

5157. By Raphael Collet (OpenERP)

[MERGE] from trunk

5158. By Raphael Collet (OpenERP)

[IMP] scope: rename method SUDO() into sudo()

5159. By Raphael Collet (OpenERP)

[FIX] new fields: fix scope mess in related fields, and evaluate name_get() in sudo mode in convert_to_read()

5160. By Raphael Collet (OpenERP)

[FIX] fields2: add cache converter on text fields, and remove it from binary fields
 - this fixes two broken tests in test_impex
 - change type of field 'domain' on 'ir.rule' to 'binary' (value is a Python list)

5161. By Raphael Collet (OpenERP)

[FIX] fields: add missing comma in tuple

5162. By Raphael Collet (OpenERP)

[FIX] orm: method name_get() must evaluate display_name in the current scope, not self's own scope

5163. By Raphael Collet (OpenERP)

[REF] orm: make AccessError and MissingError children classes of except_orm
Why? Because some modules handle access right errors by catching except_orm!

5164. By Raphael Collet (OpenERP)

[FIX] orm: before getting records in cache, always make sure the record to read is in cache!

5165. By Raphael Collet (OpenERP)

[IMP] orm: improve method check_field_access_rights()

5166. By Raphael Collet (OpenERP)

[FIX] fields2: when setting related fields, do not assign null records

5167. By Raphael Collet (OpenERP)

[IMP] orm: add properties _cr, _uid, _context on recordsets (bw compatibility w/ browse records)

5168. By Stephane Wirtel (OpenERP)

[IMP] Keep the backward compatibility with the old version of OpenERP

5169. By Stephane Wirtel (OpenERP)

[FIX] Pass the rights parameter to the right method in the wrapper

5170. By Raphael Collet (OpenERP)

[MERGE] from trunk

5171. By Raphael Collet (OpenERP)

[IMP] orm: simplify the query for reading 'classic_write' columns

5172. By Stephane Wirtel (OpenERP)

[FIX] On OSX, the system has a lot of Bitmap fonts, and in this case, Reportlab
can not load the 'head' table from the structure of the TTF file. There is no
good way to check if a TTF file is an old style or new style.

5173. By Raphael Collet (OpenERP)

[FIX] orm: add access rights check for fields in read()

5174. By Raphael Collet (OpenERP)

[IMP] api: add method decorators 'old' and 'new' to manually define api implementations
This allows to remove the hack that handles the case of method 'read': its old-style
implementation is given explicitly.

5175. By Raphael Collet (OpenERP)

[FIX] fields: decorate functions used in fields (selection, domain) to make them callable with both apis

5176. By Raphael Collet (OpenERP)

[IMP] api: make scope getter function more specific

5177. By Raphael Collet (OpenERP)

[MERGE] from trunk

5178. By Stephane Wirtel (OpenERP)

[MERGE] from trunk

5179. By Raphael Collet (OpenERP)

[FIX] fields: add api wrapper for selection function in function fields

5180. By Raphael Collet (OpenERP)

[IMP] orm: improve code of method create()

5181. By Raphael Collet (OpenERP)

[MERGE] from trunk

5182. By Raphael Collet (OpenERP)

[IMP] orm: modify the chain of calls for reading records such that overridden read() is taken into account
 - motivation: if method read() is overridden, field.__get__() uses it!
 - records.read() calls records._read_from_database() to store values in cache,
   then it retrieves values from the cache (included computed fields)
 - field.__get__() retrieves stored fields by calling record._prefetch_field(),
   which calls records.read() for stored fields only (no call loop!)

5183. By Raphael Collet (OpenERP)

[FIX] orm: in _read_from_database(), avoid getting post fields with empty ids list

5184. By Raphael Collet (OpenERP)

[FIX] scope: make the draft switch more robust in case of exceptions

5185. By Raphael Collet (OpenERP)

[IMP] api: make the decorators 'old' and 'new' more intuitive

5186. By Raphael Collet (OpenERP)

[IMP] orm scope: use a weak dictionary to store the set of scopes

Unmerged revisions

5186. By Raphael Collet (OpenERP)

[IMP] orm scope: use a weak dictionary to store the set of scopes

5185. By Raphael Collet (OpenERP)

[IMP] api: make the decorators 'old' and 'new' more intuitive

5184. By Raphael Collet (OpenERP)

[FIX] scope: make the draft switch more robust in case of exceptions

5183. By Raphael Collet (OpenERP)

[FIX] orm: in _read_from_database(), avoid getting post fields with empty ids list

5182. By Raphael Collet (OpenERP)

[IMP] orm: modify the chain of calls for reading records such that overridden read() is taken into account
 - motivation: if method read() is overridden, field.__get__() uses it!
 - records.read() calls records._read_from_database() to store values in cache,
   then it retrieves values from the cache (included computed fields)
 - field.__get__() retrieves stored fields by calling record._prefetch_field(),
   which calls records.read() for stored fields only (no call loop!)

5181. By Raphael Collet (OpenERP)

[MERGE] from trunk

5180. By Raphael Collet (OpenERP)

[IMP] orm: improve code of method create()

5179. By Raphael Collet (OpenERP)

[FIX] fields: add api wrapper for selection function in function fields

5178. By Stephane Wirtel (OpenERP)

[MERGE] from trunk

5177. By Raphael Collet (OpenERP)

[MERGE] from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/03_module_dev_02.rst'
2--- doc/03_module_dev_02.rst 2013-06-19 09:13:32 +0000
3+++ doc/03_module_dev_02.rst 2014-01-22 16:19:54 +0000
4@@ -615,6 +615,7 @@
5 reference. :guilabel:`relation` is the table to look up that
6 reference in.
7
8+.. _fields-functional:
9
10 Functional Fields
11 +++++++++++++++++
12
13=== modified file 'doc/03_module_dev_03.rst'
14--- doc/03_module_dev_03.rst 2013-09-04 12:58:42 +0000
15+++ doc/03_module_dev_03.rst 2014-01-22 16:19:54 +0000
16@@ -70,15 +70,21 @@
17 On Change
18 +++++++++
19
20-The on_change attribute defines a method that is called when the content of a view field has changed.
21+The on_change attribute defines a method that is called when the
22+content of a view field has changed.
23
24-This method takes at least arguments: cr, uid, ids, which are the three classical arguments and also the context dictionary. You can add parameters to the method. They must correspond to other fields defined in the view, and must also be defined in the XML with fields defined this way::
25+This method takes at least arguments: cr, uid, ids, which are the
26+three classical arguments and also the context dictionary. You can add
27+parameters to the method. They must correspond to other fields defined
28+in the view, and must also be defined in the XML with fields defined
29+this way::
30
31 <field name="name_of_field" on_change="name_of_method(other_field'_1_', ..., other_field'_n_')"/>
32
33 The example below is from the sale order view.
34
35-You can use the 'context' keyword to access data in the context that can be used as params of the function.::
36+You can use the 'context' keyword to access data in the context that
37+can be used as params of the function.::
38
39 <field name="shop_id" on_change="onchange_shop_id(shop_id)"/>
40
41@@ -100,7 +106,10 @@
42 return {'value':v}
43
44
45-When editing the shop_id form field, the onchange_shop_id method of the sale_order object is called and returns a dictionary where the 'value' key contains a dictionary of the new value to use in the 'project_id', 'pricelist_id' and 'payment_default_id' fields.
46+When editing the shop_id form field, the onchange_shop_id method of
47+the sale_order object is called and returns a dictionary where the
48+'value' key contains a dictionary of the new value to use in the
49+'project_id', 'pricelist_id' and 'payment_default_id' fields.
50
51 Note that it is possible to change more than just the values of
52 fields. For example, it is possible to change the value of some fields
53
54=== modified file 'doc/api_models.rst'
55--- doc/api_models.rst 2012-11-11 02:10:22 +0000
56+++ doc/api_models.rst 2014-01-22 16:19:54 +0000
57@@ -1,7 +1,21 @@
58
59-ORM and models
60---------------
61+ORM and Models
62+==============
63
64 .. automodule:: openerp.osv.orm
65 :members:
66 :undoc-members:
67+
68+Scope Management
69+================
70+
71+.. automodule:: openerp.osv.scope
72+ :members:
73+ :undoc-members:
74+
75+API Decorators
76+==============
77+
78+.. automodule:: openerp.osv.api
79+ :members:
80+ :undoc-members:
81
82=== modified file 'doc/index.rst'
83--- doc/index.rst 2013-07-31 15:16:36 +0000
84+++ doc/index.rst 2014-01-22 16:19:54 +0000
85@@ -38,9 +38,10 @@
86 .. toctree::
87 :maxdepth: 1
88
89- orm-methods.rst
90- api_models.rst
91- routing.rst
92+ new_api
93+ orm-methods
94+ api_models
95+ routing
96
97 Changelog
98 '''''''''
99
100=== added file 'doc/new_api.rst'
101--- doc/new_api.rst 1970-01-01 00:00:00 +0000
102+++ doc/new_api.rst 2014-01-22 16:19:54 +0000
103@@ -0,0 +1,138 @@
104+==================
105+High-level ORM API
106+==================
107+
108+.. _compute:
109+
110+Computed fields: defaults and function fields
111+=============================================
112+
113+The high-level API attempts to unify concepts of programmatic value generation
114+for function fields (stored or not) and default values through the use of
115+computed fields.
116+
117+Fields are marked as computed by setting their ``compute`` attribute to the
118+name of the method used to compute then::
119+
120+ has_sibling = fields.Integer(compute='compute_has_sibling')
121+
122+by default computation methods behave as simple defaults in case no
123+corresponding value is found in the database::
124+
125+ def default_number_of_employees(self):
126+ self.number_of_employees = 1
127+
128+.. todo::
129+
130+ literal defaults::
131+
132+ has_sibling = fields.Integer(compute=fields.default(1))
133+
134+but they can also be used for computed fields by specifying fields used for
135+the computation. The dependencies can be dotted for "cascading" through
136+related models::
137+
138+ @api.depends('parent_id.children_count')
139+ def compute_has_sibling(self):
140+ self.has_sibling = self.parent_id.children_count >= 2
141+
142+.. todo::
143+
144+ function-based::
145+
146+ has_sibling = fields.Integer()
147+ @has_sibling.computer
148+ @api.depends('parent_id.children_count')
149+ def compute_has_sibling(self):
150+ self.has_sibling = self.parent_id.children_count >= 2
151+
152+note that computation methods (defaults or others) do not *return* a value,
153+they *set* values the current object. This means the high-level API does not
154+need :ref:`an explicit multi <fields-functional>`: a ``multi`` method is
155+simply one which computes several values at once::
156+
157+ @api.depends('company_id')
158+ def compute_relations(self):
159+ self.computed_company = self.company_id
160+ self.computed_companies = self.company_id.to_recordset()
161+
162+Automatic onchange
163+==================
164+
165+Using to the improved and expanded :ref:`computed fields <compute>`, the
166+high-level ORM API is able to infer the effect of fields on
167+one another, and thus automatically provide a basic form of onchange without
168+having to implement it by hand, or implement dozens of onchange functions to
169+get everything right.
170+
171+
172+
173+
174+.. todo::
175+
176+ deferred records::
177+
178+ partner = Partner.record(42, defer=True)
179+ partner.name = "foo"
180+ partner.user_id = juan
181+ partner.save() # only saved to db here
182+
183+ with scope.defer():
184+ # all records in this scope or children scopes are deferred
185+ # until corresponding scope poped or until *this* scope poped?
186+ partner = Partner.record(42)
187+ partner.name = "foo"
188+ partner.user_id = juan
189+ # saved here, also for recordset &al, ~transaction
190+
191+ # temp deferment, maybe simpler? Or for bulk operations?:
192+ with Partner.record(42) as partner:
193+ partner.name = "foo"
194+ partner.user_id = juan
195+
196+ ``id = False`` => always defered? null v draft?
197+
198+.. todo:: keyword arguments passed positionally (common for context, completely breaks everything)
199+
200+.. todo:: optional arguments (report_aged_receivable)
201+
202+.. todo:: non-id ids? (mail thread_id)
203+
204+.. todo:: partial signatures on overrides (e.g. message_post)
205+
206+.. todo::
207+
208+ ::
209+
210+ field = fields.Char()
211+
212+ @field.computer
213+ def foo(self):
214+ "compute foo here"
215+
216+ ~
217+
218+ ::
219+
220+ field = fields.Char(compute='foo')
221+
222+ def foo(self):
223+ "compute foo here"
224+
225+.. todo:: doc
226+
227+.. todo:: incorrect dependency spec?
228+
229+.. todo:: dynamic dependencies?
230+
231+ ::
232+
233+ @api.depends(???)
234+ def foo(self)
235+ self.a = self[self.b]
236+
237+.. todo:: recursive onchange
238+
239+ Country & state. Change country -> remove state; set state -> set country
240+
241+.. todo:: onchange list affected?
242
243=== modified file 'openerp/__init__.py'
244--- openerp/__init__.py 2013-09-09 23:19:46 +0000
245+++ openerp/__init__.py 2014-01-22 16:19:54 +0000
246@@ -58,6 +58,21 @@
247 import sql_db
248 import tools
249 import workflow
250+
251+# model classes
252+from openerp.osv.orm import BaseModel, AbstractModel, Model, TransientModel
253+
254+# field classes
255+from openerp.osv import fields2 as fields
256+
257+# api module and decorators
258+from openerp.osv import api
259+from openerp.osv.api import model, multi, one, constrains, depends, returns
260+
261+# scope proxy
262+from openerp.osv.scope import proxy as scope
263+
264+
265 # backward compatilbility
266 # TODO: This is for the web addons, can be removed later.
267 wsgi = service
268
269=== modified file 'openerp/addons/base/__openerp__.py'
270--- openerp/addons/base/__openerp__.py 2013-10-06 15:18:27 +0000
271+++ openerp/addons/base/__openerp__.py 2014-01-22 16:19:54 +0000
272@@ -39,7 +39,6 @@
273 'res/res_country_data.xml',
274 'security/base_security.xml',
275 'base_menu.xml',
276- 'res/res_security.xml',
277 'res/res_config.xml',
278 'res/res.country.state.csv',
279 'ir/ir_actions.xml',
280@@ -81,7 +80,7 @@
281 'res/res_users_view.xml',
282 'res/res_partner_data.xml',
283 'res/ir_property_view.xml',
284- 'security/base_security.xml',
285+ 'res/res_security.xml',
286 'security/ir.model.access.csv',
287 ],
288 'demo': [
289
290=== modified file 'openerp/addons/base/base.sql'
291--- openerp/addons/base/base.sql 2013-11-08 13:02:08 +0000
292+++ openerp/addons/base/base.sql 2014-01-22 16:19:54 +0000
293@@ -124,16 +124,6 @@
294 primary key(id)
295 );
296
297-CREATE TABLE ir_ui_menu (
298- id serial NOT NULL,
299- parent_id int references ir_ui_menu on delete set null,
300- name varchar(64) DEFAULT ''::varchar NOT NULL,
301- icon varchar(64) DEFAULT ''::varchar,
302- primary key (id)
303-);
304-
305-select setval('ir_ui_menu_id_seq', 2);
306-
307 ---------------------------------
308 -- Res users
309 ---------------------------------
310@@ -171,7 +161,6 @@
311 create index res_groups_users_rel_uid_idx on res_groups_users_rel (uid);
312 create index res_groups_users_rel_gid_idx on res_groups_users_rel (gid);
313
314-
315 ---------------------------------
316 -- Workflows
317 ---------------------------------
318@@ -264,10 +253,6 @@
319
320 CREATE TABLE ir_module_category (
321 id serial NOT NULL,
322- create_uid integer references res_users on delete set null,
323- create_date timestamp without time zone,
324- write_date timestamp without time zone,
325- write_uid integer references res_users on delete set null,
326 parent_id integer REFERENCES ir_module_category ON DELETE SET NULL,
327 name character varying(128) NOT NULL,
328 primary key(id)
329@@ -276,10 +261,6 @@
330
331 CREATE TABLE ir_module_module (
332 id serial NOT NULL,
333- create_uid integer references res_users on delete set null,
334- create_date timestamp without time zone,
335- write_date timestamp without time zone,
336- write_uid integer references res_users on delete set null,
337 website character varying(256),
338 summary character varying(256),
339 name character varying(128) NOT NULL,
340@@ -304,10 +285,6 @@
341
342 CREATE TABLE ir_module_module_dependency (
343 id serial NOT NULL,
344- create_uid integer references res_users on delete set null,
345- create_date timestamp without time zone,
346- write_date timestamp without time zone,
347- write_uid integer references res_users on delete set null,
348 name character varying(128),
349 version_pattern character varying(128) default NULL,
350 module_id integer REFERENCES ir_module_module ON DELETE cascade,
351@@ -345,10 +322,6 @@
352
353 CREATE TABLE ir_model_data (
354 id serial NOT NULL,
355- create_uid integer,
356- create_date timestamp without time zone,
357- write_date timestamp without time zone,
358- write_uid integer,
359 noupdate boolean,
360 name varchar NOT NULL,
361 date_init timestamp without time zone,
362@@ -364,10 +337,6 @@
363 -- - for a constraint: type is 'u' (this is the convention PostgreSQL uses).
364 CREATE TABLE ir_model_constraint (
365 id serial NOT NULL,
366- create_uid integer,
367- create_date timestamp without time zone,
368- write_date timestamp without time zone,
369- write_uid integer,
370 date_init timestamp without time zone,
371 date_update timestamp without time zone,
372 module integer NOT NULL references ir_module_module on delete restrict,
373@@ -380,10 +349,6 @@
374 -- (so they can be removed when the module is uninstalled).
375 CREATE TABLE ir_model_relation (
376 id serial NOT NULL,
377- create_uid integer,
378- create_date timestamp without time zone,
379- write_date timestamp without time zone,
380- write_uid integer,
381 date_init timestamp without time zone,
382 date_update timestamp without time zone,
383 module integer NOT NULL references ir_module_module on delete restrict,
384@@ -409,4 +374,4 @@
385 select setval('res_company_id_seq', 2);
386 select setval('res_users_id_seq', 2);
387 select setval('res_partner_id_seq', 2);
388-select setval('res_currency_id_seq', 2);
389\ No newline at end of file
390+select setval('res_currency_id_seq', 2);
391
392=== modified file 'openerp/addons/base/base_menu.xml'
393--- openerp/addons/base/base_menu.xml 2013-10-06 11:26:08 +0000
394+++ openerp/addons/base/base_menu.xml 2014-01-22 16:19:54 +0000
395@@ -28,6 +28,10 @@
396 <menuitem id="menu_security" name="Security" parent="menu_custom" sequence="25"/>
397 <menuitem id="menu_ir_property" name="Parameters" parent="menu_custom" sequence="24"/>
398
399+ <record model="ir.ui.menu" id="base.menu_administration">
400+ <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
401+ </record>
402+
403 <record id="action_client_base_menu" model="ir.actions.client">
404 <field name="name">Open Settings Menu</field>
405 <field name="tag">reload</field>
406
407=== modified file 'openerp/addons/base/ir/ir_actions.py'
408--- openerp/addons/base/ir/ir_actions.py 2014-01-15 20:53:57 +0000
409+++ openerp/addons/base/ir/ir_actions.py 2014-01-22 16:19:54 +0000
410@@ -324,7 +324,7 @@
411 dataobj = self.pool.get('ir.model.data')
412 data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
413 res_id = dataobj.browse(cr, uid, data_id, context).res_id
414- return self.read(cr, uid, res_id, [], context)
415+ return self.read(cr, uid, [res_id], [], context)[0]
416
417 VIEW_TYPES = [
418 ('tree', 'Tree'),
419@@ -544,7 +544,7 @@
420 'sequence': 5,
421 'code': """# You can use the following variables:
422 # - self: ORM model of the record on which the action is triggered
423-# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
424+# - object: Record on which the action is triggered if there is one, otherwise None
425 # - pool: ORM model pool (i.e. self.pool)
426 # - cr: database cursor
427 # - uid: current user id
428@@ -799,7 +799,7 @@
429 def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
430 if not action.action_id:
431 raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
432- return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
433+ return self.pool[action.action_id.type].read(cr, uid, [action.action_id.id], context=context)[0]
434
435 def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
436 eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'
437@@ -1038,10 +1038,10 @@
438 wizard.write({'state': 'done'})
439
440 # Load action
441- act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)
442+ act_type = wizard.action_id.type
443
444- res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)
445- if act_type['type'] != 'ir.actions.act_window':
446+ res = self.pool[act_type].read(cr, uid, [wizard.action_id.id], [], context=context)[0]
447+ if act_type != 'ir.actions.act_window':
448 return res
449 res.setdefault('context','{}')
450 res['nodestroy'] = True
451
452=== modified file 'openerp/addons/base/ir/ir_attachment.py'
453--- openerp/addons/base/ir/ir_attachment.py 2014-01-07 14:15:24 +0000
454+++ openerp/addons/base/ir/ir_attachment.py 2014-01-22 16:19:54 +0000
455@@ -242,7 +242,7 @@
456 # performed in batch as much as possible.
457 ima = self.pool.get('ir.model.access')
458 for model, targets in model_attachments.iteritems():
459- if not self.pool.get(model):
460+ if model not in self.pool:
461 continue
462 if not ima.check(cr, uid, model, 'read', False):
463 # remove all corresponding attachment ids
464@@ -266,7 +266,7 @@
465 if isinstance(ids, (int, long)):
466 ids = [ids]
467 self.check(cr, uid, ids, 'read', context=context)
468- return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context, load)
469+ return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context=context, load=load)
470
471 def write(self, cr, uid, ids, vals, context=None):
472 if isinstance(ids, (int, long)):
473
474=== modified file 'openerp/addons/base/ir/ir_cron.py'
475--- openerp/addons/base/ir/ir_cron.py 2013-04-22 09:36:55 +0000
476+++ openerp/addons/base/ir/ir_cron.py 2014-01-22 16:19:54 +0000
477@@ -149,10 +149,10 @@
478 except Exception, e:
479 self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
480
481- def _process_job(self, job_cr, job, cron_cr):
482+ def _process_job(self, cr, job, cron_cr):
483 """ Run a given job taking care of the repetition.
484
485- :param job_cr: cursor to use to execute the job, safe to commit/rollback
486+ :param cr: cursor to use to execute the job, safe to commit/rollback
487 :param job: job to be run (as a dictionary).
488 :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
489 must not be committed/rolled back!
490@@ -167,7 +167,7 @@
491 if numbercall > 0:
492 numbercall -= 1
493 if not ok or job['doall']:
494- self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
495+ self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
496 if numbercall:
497 nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
498 ok = True
499@@ -176,9 +176,10 @@
500 addsql = ', active=False'
501 cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
502 (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
503+ self.invalidate_cache(['nextcall', 'numbercall', 'active'], [job['id']])
504
505 finally:
506- job_cr.commit()
507+ cr.commit()
508 cron_cr.commit()
509
510 @classmethod
511
512=== modified file 'openerp/addons/base/ir/ir_mail_server.py'
513--- openerp/addons/base/ir/ir_mail_server.py 2013-11-23 11:30:53 +0000
514+++ openerp/addons/base/ir/ir_mail_server.py 2014-01-22 16:19:54 +0000
515@@ -461,21 +461,18 @@
516 mdir.add(message.as_string(True))
517 return message_id
518
519+ smtp = None
520 try:
521 smtp = self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption or False, smtp_debug)
522 smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
523 finally:
524- try:
525- # Close Connection of SMTP Server
526+ if smtp is not None:
527 smtp.quit()
528- except Exception:
529- # ignored, just a consequence of the previous exception
530- pass
531 except Exception, e:
532 msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server),
533 e.__class__.__name__,
534 tools.ustr(e))
535- _logger.exception(msg)
536+ _logger.error(msg)
537 raise MailDeliveryException(_("Mail Delivery Failed"), msg)
538 return message_id
539
540
541=== modified file 'openerp/addons/base/ir/ir_model.py'
542--- openerp/addons/base/ir/ir_model.py 2013-11-27 11:07:57 +0000
543+++ openerp/addons/base/ir/ir_model.py 2014-01-22 16:19:54 +0000
544@@ -28,12 +28,12 @@
545 import openerp.modules.registry
546 from openerp import SUPERUSER_ID
547 from openerp import tools
548-from openerp.osv import fields,osv
549-from openerp.osv.orm import Model
550+from openerp.osv import fields, osv
551+from openerp.osv.orm import BaseModel, Model, except_orm
552+from openerp.osv.scope import proxy as scope
553+from openerp.tools import config
554 from openerp.tools.safe_eval import safe_eval as eval
555-from openerp.tools import config
556 from openerp.tools.translate import _
557-from openerp.osv.orm import except_orm, browse_record
558
559 _logger = logging.getLogger(__name__)
560
561@@ -133,15 +133,10 @@
562 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
563 ]
564
565- # overridden to allow searching both on model name (model field)
566- # and model description (name field)
567- def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
568- if args is None:
569- args = []
570- domain = args + ['|', ('model', operator, name), ('name', operator, name)]
571- return self.name_get(cr, name_get_uid or uid,
572- super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
573- context=context)
574+ def _search_display_name(self, operator, value):
575+ # overridden to allow searching both on model name (model field) and
576+ # model description (name field)
577+ return ['|', ('model', operator, value), ('name', operator, value)]
578
579 def _drop_table(self, cr, uid, ids, context=None):
580 for model in self.browse(cr, uid, ids, context):
581@@ -207,7 +202,8 @@
582 _custom = True
583 x_custom_model._name = model
584 x_custom_model._module = False
585- a = x_custom_model.create_instance(self.pool, cr)
586+ with scope(cr, SUPERUSER_ID, None):
587+ a = x_custom_model._build_model(self.pool, cr)
588 if not a._columns:
589 x_name = 'id'
590 elif 'x_name' in a._columns.keys():
591@@ -625,8 +621,8 @@
592 """ Check if a specific group has the access mode to the specified model"""
593 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
594
595- if isinstance(model, browse_record):
596- assert model._table_name == 'ir.model', 'Invalid model object'
597+ if isinstance(model, BaseModel):
598+ assert model._name == 'ir.model', 'Invalid model object'
599 model_name = model.name
600 else:
601 model_name = model
602@@ -684,8 +680,8 @@
603
604 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
605
606- if isinstance(model, browse_record):
607- assert model._table_name == 'ir.model', 'Invalid model object'
608+ if isinstance(model, BaseModel):
609+ assert model._name == 'ir.model', 'Invalid model object'
610 model_name = model.model
611 else:
612 model_name = model
613@@ -753,6 +749,7 @@
614 pass
615
616 def call_cache_clearing_methods(self, cr):
617+ scope.invalidate_all()
618 self.check.clear_cache(self) # clear the cache of check function
619 for model, method in self.__cache_clearing_methods:
620 if model in self.pool:
621@@ -761,19 +758,19 @@
622 #
623 # Check rights on actions
624 #
625- def write(self, cr, uid, *args, **argv):
626- self.call_cache_clearing_methods(cr)
627- res = super(ir_model_access, self).write(cr, uid, *args, **argv)
628- return res
629-
630- def create(self, cr, uid, *args, **argv):
631- self.call_cache_clearing_methods(cr)
632- res = super(ir_model_access, self).create(cr, uid, *args, **argv)
633- return res
634-
635- def unlink(self, cr, uid, *args, **argv):
636- self.call_cache_clearing_methods(cr)
637- res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)
638+ def write(self, cr, uid, ids, values, context=None):
639+ self.call_cache_clearing_methods(cr)
640+ res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
641+ return res
642+
643+ def create(self, cr, uid, values, context=None):
644+ self.call_cache_clearing_methods(cr)
645+ res = super(ir_model_access, self).create(cr, uid, values, context=context)
646+ return res
647+
648+ def unlink(self, cr, uid, ids, context=None):
649+ self.call_cache_clearing_methods(cr)
650+ res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
651 return res
652
653 class ir_model_data(osv.osv):
654@@ -829,8 +826,8 @@
655 'date_init': fields.datetime('Init Date')
656 }
657 _defaults = {
658- 'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
659- 'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
660+ 'date_init': fields.datetime.now,
661+ 'date_update': fields.datetime.now,
662 'noupdate': False,
663 'module': ''
664 }
665@@ -840,12 +837,11 @@
666
667 def __init__(self, pool, cr):
668 osv.osv.__init__(self, pool, cr)
669- self.doinit = True
670 # also stored in pool to avoid being discarded along with this osv instance
671 if getattr(pool, 'model_data_reference_ids', None) is None:
672 self.pool.model_data_reference_ids = {}
673-
674- self.loads = self.pool.model_data_reference_ids
675+ # put loads on the class, in order to share it among all instances
676+ type(self).loads = self.pool.model_data_reference_ids
677
678 def _auto_init(self, cr, context=None):
679 super(ir_model_data, self)._auto_init(cr, context)
680@@ -867,7 +863,7 @@
681 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
682 data_id = self._get_id(cr, uid, module, xml_id)
683 #assuming data_id is not False, as it was checked upstream
684- res = self.read(cr, uid, data_id, ['model', 'res_id'])
685+ res = self.read(cr, uid, [data_id], ['model', 'res_id'])[0]
686 if not res['res_id']:
687 raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
688 return res['model'], res['res_id']
689@@ -925,8 +921,6 @@
690 if xml_id and ('.' in xml_id):
691 assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % xml_id
692 module, xml_id = xml_id.split('.')
693- if (not xml_id) and (not self.doinit):
694- return False
695 action_id = False
696 if xml_id:
697 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
698@@ -996,8 +990,8 @@
699 if xml_id and res_id:
700 self.loads[(module, xml_id)] = (model, res_id)
701 for table, inherit_field in model_obj._inherits.iteritems():
702- inherit_id = model_obj.read(cr, uid, res_id,
703- [inherit_field])[inherit_field]
704+ inherit_id = model_obj.read(cr, uid, [res_id],
705+ [inherit_field])[0][inherit_field]
706 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
707 return res_id
708
709@@ -1020,11 +1014,12 @@
710
711 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
712 res = cr.fetchone()
713+ ir_values_obj = openerp.registry(cr.dbname)['ir.values']
714 if not res:
715- ir_values_obj = openerp.registry(cr.dbname)['ir.values']
716 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
717 elif xml_id:
718 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
719+ ir_values_obj.invalidate_cache(['value'])
720 return True
721
722 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
723@@ -1066,6 +1061,7 @@
724 cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
725 wkf_todo.extend(cr.fetchall())
726 cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
727+ self.pool.get('workflow.transition').invalidate_cache()
728
729 for model,res_id in wkf_todo:
730 try:
731
732=== modified file 'openerp/addons/base/ir/ir_rule.py'
733--- openerp/addons/base/ir/ir_rule.py 2013-03-29 14:07:23 +0000
734+++ openerp/addons/base/ir/ir_rule.py 2014-01-22 16:19:54 +0000
735@@ -78,7 +78,7 @@
736 'global': fields.function(_get_value, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone"),
737 'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'),
738 'domain_force': fields.text('Domain'),
739- 'domain': fields.function(_domain_force_get, string='Domain', type='text'),
740+ 'domain': fields.function(_domain_force_get, string='Domain', type='binary'),
741 'perm_read': fields.boolean('Apply for Read'),
742 'perm_write': fields.boolean('Apply for Write'),
743 'perm_create': fields.boolean('Apply for Create'),
744@@ -127,7 +127,7 @@
745 group_domains = {} # map: group -> list of domains
746 for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
747 # read 'domain' as UID to have the correct eval context for the rule.
748- rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
749+ rule_domain = self.read(cr, uid, [rule.id], ['domain'])[0]['domain']
750 dom = expression.normalize_domain(rule_domain)
751 for group in rule.groups:
752 if group in user.groups_id:
753
754=== modified file 'openerp/addons/base/ir/ir_sequence.py'
755--- openerp/addons/base/ir/ir_sequence.py 2013-08-23 09:56:35 +0000
756+++ openerp/addons/base/ir/ir_sequence.py 2014-01-22 16:19:54 +0000
757@@ -234,15 +234,15 @@
758 'sec': time.strftime('%S', t),
759 }
760
761- def _next(self, cr, uid, seq_ids, context=None):
762- if not seq_ids:
763+ def _next(self, cr, uid, ids, context=None):
764+ if not ids:
765 return False
766 if context is None:
767 context = {}
768 force_company = context.get('force_company')
769 if not force_company:
770 force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
771- sequences = self.read(cr, uid, seq_ids, ['name','company_id','implementation','number_next','prefix','suffix','padding'])
772+ sequences = self.read(cr, uid, ids, ['name','company_id','implementation','number_next','prefix','suffix','padding'])
773 preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ]
774 seq = preferred_sequences[0] if preferred_sequences else sequences[0]
775 if seq['implementation'] == 'standard':
776@@ -251,6 +251,7 @@
777 else:
778 cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],))
779 cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],))
780+ self.invalidate_cache(['number_next'], [seq['id']])
781 d = self._interpolation_dict()
782 try:
783 interpolated_prefix = self._interpolate(seq['prefix'], d)
784
785=== modified file 'openerp/addons/base/ir/ir_translation.py'
786--- openerp/addons/base/ir/ir_translation.py 2014-01-15 20:53:57 +0000
787+++ openerp/addons/base/ir/ir_translation.py 2014-01-22 16:19:54 +0000
788@@ -168,11 +168,11 @@
789 else:
790 model_name, field = record.name.split(',')
791 model = self.pool.get(model_name)
792- if model and model.exists(cr, uid, record.res_id, context=context):
793+ if model is not None:
794 # Pass context without lang, need to read real stored field, not translation
795 context_no_lang = dict(context, lang=None)
796- result = model.read(cr, uid, record.res_id, [field], context=context_no_lang)
797- res[record.id] = result[field] if result else False
798+ result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang)
799+ res[record.id] = result[0][field] if result else False
800 return res
801
802 def _set_src(self, cr, uid, id, name, value, args, context=None):
803
804=== modified file 'openerp/addons/base/ir/ir_ui_menu.py'
805--- openerp/addons/base/ir/ir_ui_menu.py 2013-10-06 13:24:24 +0000
806+++ openerp/addons/base/ir/ir_ui_menu.py 2014-01-22 16:19:54 +0000
807@@ -36,46 +36,48 @@
808 _name = 'ir.ui.menu'
809
810 def __init__(self, *args, **kwargs):
811- self.cache_lock = threading.RLock()
812- self._cache = {}
813+ cls = type(self)
814+ cls._menu_cache_lock = threading.RLock()
815+ cls._menu_cache = {}
816 super(ir_ui_menu, self).__init__(*args, **kwargs)
817 self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
818
819 def clear_cache(self):
820- with self.cache_lock:
821+ with self._menu_cache_lock:
822 # radical but this doesn't frequently happen
823- if self._cache:
824+ if self._menu_cache:
825 # Normally this is done by openerp.tools.ormcache
826 # but since we do not use it, set it by ourself.
827 self.pool._any_cache_cleared = True
828- self._cache = {}
829+ self._menu_cache.clear()
830
831 def _filter_visible_menus(self, cr, uid, ids, context=None):
832 """Filters the give menu ids to only keep the menu items that should be
833 visible in the menu hierarchy of the current user.
834 Uses a cache for speeding up the computation.
835 """
836- with self.cache_lock:
837+ with self._menu_cache_lock:
838 modelaccess = self.pool.get('ir.model.access')
839- user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])
840+ user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
841+ user_groups = set(user.groups_id.unbrowse())
842 result = []
843 for menu in self.browse(cr, uid, ids, context=context):
844 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
845 key = (cr.dbname, menu.id, tuple(user_groups))
846- if key in self._cache:
847- if self._cache[key]:
848+ if key in self._menu_cache:
849+ if self._menu_cache[key]:
850 result.append(menu.id)
851 #elif not menu.groups_id and not menu.action:
852 # result.append(menu.id)
853 continue
854
855- self._cache[key] = False
856+ self._menu_cache[key] = False
857 if menu.groups_id:
858 restrict_to_groups = [g.id for g in menu.groups_id]
859 if not user_groups.intersection(restrict_to_groups):
860 continue
861 #result.append(menu.id)
862- #self._cache[key] = True
863+ #self._menu_cache[key] = True
864 #continue
865
866 if menu.action:
867@@ -99,7 +101,7 @@
868 continue
869
870 result.append(menu.id)
871- self._cache[key] = True
872+ self._menu_cache[key] = True
873 return result
874
875 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
876@@ -153,13 +155,13 @@
877 parent_path = ''
878 return parent_path + elmt.name
879
880- def create(self, *args, **kwargs):
881+ def create(self, cr, uid, values, context=None):
882 self.clear_cache()
883- return super(ir_ui_menu, self).create(*args, **kwargs)
884+ return super(ir_ui_menu, self).create(cr, uid, values, context=context)
885
886- def write(self, *args, **kwargs):
887+ def write(self, cr, uid, ids, values, context=None):
888 self.clear_cache()
889- return super(ir_ui_menu, self).write(*args, **kwargs)
890+ return super(ir_ui_menu, self).write(cr, uid, ids, values, context=context)
891
892 def unlink(self, cr, uid, ids, context=None):
893 # Detach children and promote them to top-level, because it would be unwise to
894
895=== modified file 'openerp/addons/base/ir/ir_ui_view.py'
896--- openerp/addons/base/ir/ir_ui_view.py 2013-10-06 13:24:24 +0000
897+++ openerp/addons/base/ir/ir_ui_view.py 2014-01-22 16:19:54 +0000
898@@ -60,11 +60,8 @@
899 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
900 return result
901
902- _columns = {
903- 'name': fields.char('View Name', required=True),
904- 'model': fields.char('Object', size=64, required=True, select=True),
905- 'priority': fields.integer('Sequence', required=True),
906- 'type': fields.function(_type_field, type='selection', selection=[
907+ def _valid_view_types(self, cr, uid, context=None):
908+ return [
909 ('tree','Tree'),
910 ('form','Form'),
911 ('mdx','mdx'),
912@@ -73,7 +70,16 @@
913 ('diagram','Diagram'),
914 ('gantt', 'Gantt'),
915 ('kanban', 'Kanban'),
916- ('search','Search')], string='View Type', required=True, select=True, store=True),
917+ ('search', 'Search'),
918+ ]
919+
920+ _columns = {
921+ 'name': fields.char('View Name', required=True),
922+ 'model': fields.char('Object', size=64, required=True, select=True),
923+ 'priority': fields.integer('Sequence', required=True),
924+ 'type': fields.function(_type_field, type='selection',
925+ selection=lambda self, *args, **kwargs: self._valid_view_types(*args, **kwargs),
926+ string='View Type', required=True, select=True, store=True),
927 'arch': fields.text('View Architecture', required=True),
928 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
929 'field_parent': fields.char('Child Field',size=64),
930@@ -119,7 +125,7 @@
931 """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
932 its inherited views, by rendering it using ``fields_view_get()``.
933
934- @param browse_record view: view to validate
935+ @param Record view: view to validate
936 @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
937 if no error occurred, else False.
938 """
939@@ -252,7 +258,7 @@
940 _Destination_Field=node_key
941 flag = True
942
943- datas = _Model_Obj.read(cr, uid, id, [],context)
944+ datas = _Model_Obj.read(cr, uid, [id], context=context)[0]
945 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
946 if a[_Source_Field] or a[_Destination_Field]:
947 nodes_name.append((a['id'],a['name']))
948
949=== modified file 'openerp/addons/base/ir/ir_values.py'
950--- openerp/addons/base/ir/ir_values.py 2013-06-13 17:39:00 +0000
951+++ openerp/addons/base/ir/ir_values.py 2014-01-22 16:19:54 +0000
952@@ -20,6 +20,7 @@
953 ##############################################################################
954 import pickle
955
956+from openerp import tools
957 from openerp.osv import osv, fields
958 from openerp.osv.orm import except_orm
959
960@@ -188,6 +189,21 @@
961 if not cr.fetchone():
962 cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
963
964+ def create(self, cr, uid, vals, context=None):
965+ res = super(ir_values, self).create(cr, uid, vals, context=context)
966+ self.get_defaults_dict.clear_cache(self)
967+ return res
968+
969+ def write(self, cr, uid, ids, vals, context=None):
970+ res = super(ir_values, self).write(cr, uid, ids, vals, context=context)
971+ self.get_defaults_dict.clear_cache(self)
972+ return res
973+
974+ def unlink(self, cr, uid, ids, context=None):
975+ res = super(ir_values, self).unlink(cr, uid, ids, context=context)
976+ self.get_defaults_dict.clear_cache(self)
977+ return res
978+
979 def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):
980 """Defines a default value for the given model and field_name. Any previous
981 default for the same scope (model, field_name, value, for_all_users, company_id, condition)
982@@ -319,6 +335,15 @@
983 (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
984 return defaults.values()
985
986+ # use ormcache: this is called a lot by BaseModel.add_default_value()!
987+ @tools.ormcache(skiparg=2)
988+ def get_defaults_dict(self, cr, uid, model, condition=False):
989+ """ Returns a dictionary mapping field names with their corresponding
990+ default value. This method simply improves the returned value of
991+ :meth:`~.get_defaults`.
992+ """
993+ return dict((f, v) for i, f, v in self.get_defaults(cr, uid, model, condition))
994+
995 def set_action(self, cr, uid, name, action_slot, model, action, res_id=False):
996 """Binds an the given action to the given model's action slot - for later
997 retrieval via :meth:`~.get_actions`. Any existing binding of the same action
998@@ -401,7 +426,7 @@
999 if field not in EXCLUDED_FIELDS]
1000 # FIXME: needs cleanup
1001 try:
1002- action_def = self.pool[action_model].read(cr, uid, int(id), fields, context)
1003+ action_def = self.pool[action_model].read(cr, uid, [int(id)], fields, context)[0]
1004 if action_def:
1005 if action_model in ('ir.actions.report.xml','ir.actions.act_window',
1006 'ir.actions.wizard'):
1007
1008=== modified file 'openerp/addons/base/module/module.py'
1009--- openerp/addons/base/module/module.py 2014-01-15 20:53:57 +0000
1010+++ openerp/addons/base/module/module.py 2014-01-22 16:19:54 +0000
1011@@ -45,7 +45,7 @@
1012 from openerp.modules.db import create_categories
1013 from openerp.tools.parse_version import parse_version
1014 from openerp.tools.translate import _
1015-from openerp.osv import fields, osv, orm
1016+from openerp.osv import osv, orm, fields, fields2, api
1017
1018 _logger = logging.getLogger(__name__)
1019
1020@@ -362,34 +362,41 @@
1021 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
1022 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
1023
1024- def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
1025+ @api.multi
1026+ def state_update(self, newstate, states_to_update, level=100):
1027 if level < 1:
1028 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
1029+
1030+ # whether some modules are installed with demo data
1031 demo = False
1032- for module in self.browse(cr, uid, ids, context=context):
1033- mdemo = False
1034+
1035+ for module in self:
1036+ # determine dependency modules to update/others
1037+ update_mods, ready_mods = self.browse(), self.browse()
1038 for dep in module.dependencies_id:
1039 if dep.state == 'unknown':
1040 raise orm.except_orm(_('Error'), _("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,))
1041- ids2 = self.search(cr, uid, [('name', '=', dep.name)])
1042- if dep.state != newstate:
1043- mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level - 1) or mdemo
1044+ if dep.depend_id.state == newstate:
1045+ ready_mods += dep.depend_id
1046 else:
1047- od = self.browse(cr, uid, ids2)[0]
1048- mdemo = od.demo or mdemo
1049-
1050+ update_mods += dep.depend_id
1051+
1052+ # update dependency modules that require it, and determine demo for module
1053+ update_demo = update_mods.state_update(newstate, states_to_update, level=level-1)
1054+ module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods)
1055+ demo = demo or module_demo
1056+
1057+ # check dependencies and update module itself
1058 self.check_external_dependencies(module.name, newstate)
1059- if not module.dependencies_id:
1060- mdemo = module.demo
1061 if module.state in states_to_update:
1062- self.write(cr, uid, [module.id], {'state': newstate, 'demo': mdemo})
1063- demo = demo or mdemo
1064+ module.write({'state': newstate, 'demo': module_demo})
1065+
1066 return demo
1067
1068 def button_install(self, cr, uid, ids, context=None):
1069
1070 # Mark the given modules to be installed.
1071- self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
1072+ self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context=context)
1073
1074 # Mark (recursively) the newly satisfied modules to also be installed
1075
1076@@ -515,7 +522,7 @@
1077
1078 def button_upgrade(self, cr, uid, ids, context=None):
1079 depobj = self.pool.get('ir.module.module.dependency')
1080- todo = self.browse(cr, uid, ids, context=context)
1081+ todo = list(self.browse(cr, uid, ids, context=context))
1082 self.update_list(cr, uid)
1083
1084 i = 0
1085@@ -646,6 +653,7 @@
1086 terp = self.get_module_info(mod.name)
1087 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
1088 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,))
1089+ self.invalidate_cache(['dependencies_id'], [mod.id])
1090 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
1091 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
1092 # Import module
1093@@ -738,6 +746,7 @@
1094 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
1095 for dep in (existing - needed):
1096 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
1097+ self.invalidate_cache(['dependencies_id'], [mod_browse.id])
1098
1099 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
1100 current_category = mod_browse.category_id
1101@@ -766,37 +775,49 @@
1102 if not mod.description:
1103 _logger.warning('module %s: description is empty !', mod.name)
1104
1105-class module_dependency(osv.osv):
1106+
1107+DEP_STATES = [
1108+ ('uninstallable', 'Uninstallable'),
1109+ ('uninstalled', 'Not Installed'),
1110+ ('installed', 'Installed'),
1111+ ('to upgrade', 'To be upgraded'),
1112+ ('to remove', 'To be removed'),
1113+ ('to install', 'To be installed'),
1114+ ('unknown', 'Unknown'),
1115+]
1116+
1117+class module_dependency(osv.Model):
1118 _name = "ir.module.module.dependency"
1119 _description = "Module dependency"
1120
1121- def _state(self, cr, uid, ids, name, args, context=None):
1122- result = {}
1123- mod_obj = self.pool.get('ir.module.module')
1124- for md in self.browse(cr, uid, ids):
1125- ids = mod_obj.search(cr, uid, [('name', '=', md.name)])
1126- if ids:
1127- result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
1128- else:
1129- result[md.id] = 'unknown'
1130- return result
1131-
1132- _columns = {
1133- # The dependency name
1134- 'name': fields.char('Name', size=128, select=True),
1135-
1136- # The module that depends on it
1137- 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
1138-
1139- 'state': fields.function(_state, type='selection', selection=[
1140- ('uninstallable', 'Uninstallable'),
1141- ('uninstalled', 'Not Installed'),
1142- ('installed', 'Installed'),
1143- ('to upgrade', 'To be upgraded'),
1144- ('to remove', 'To be removed'),
1145- ('to install', 'To be installed'),
1146- ('unknown', 'Unknown'),
1147- ], string='Status', readonly=True, select=True),
1148- }
1149+ # the dependency name
1150+ name = fields2.Char(size=128)
1151+
1152+ # the module that depends on it
1153+ module_id = fields2.Many2one('ir.module.module', 'Module', ondelete='cascade')
1154+
1155+ # the module corresponding to the dependency, and its status
1156+ depend_id = fields2.Many2one('ir.module.module', 'Dependency',
1157+ compute='_compute_depend', readonly=True, store=False)
1158+ state = fields2.Selection(DEP_STATES, string='Status',
1159+ compute='_compute_state', readonly=True, store=False)
1160+
1161+ @api.multi
1162+ @api.depends('name')
1163+ def _compute_depend(self):
1164+ # retrieve all modules corresponding to the dependency names
1165+ names = list(set(dep.name for dep in self))
1166+ mods = self.pool['ir.module.module'].search([('name', 'in', names)])
1167+
1168+ # index modules by name, and assign dependencies
1169+ name_mod = dict((mod.name, mod) for mod in mods)
1170+ for dep in self:
1171+ dep.depend_id = name_mod.get(dep.name)
1172+
1173+ @api.one
1174+ @api.depends('depend_id.state')
1175+ def _compute_state(self):
1176+ self.state = self.depend_id.state or 'unknown'
1177+
1178
1179 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1180
1181=== modified file 'openerp/addons/base/res/ir_property.py'
1182--- openerp/addons/base/res/ir_property.py 2013-06-24 08:57:31 +0000
1183+++ openerp/addons/base/res/ir_property.py 2014-01-22 16:19:54 +0000
1184@@ -21,8 +21,7 @@
1185
1186 import time
1187
1188-from openerp.osv import osv, fields
1189-from openerp.osv.orm import browse_record, browse_null
1190+from openerp.osv import osv, orm, fields
1191 from openerp.tools.misc import attrgetter
1192
1193 # -------------------------------------------------------------------------
1194@@ -97,7 +96,7 @@
1195 raise osv.except_osv('Error', 'Invalid type')
1196
1197 if field == 'value_reference':
1198- if isinstance(value, browse_record):
1199+ if isinstance(value, orm.BaseModel):
1200 value = '%s,%d' % (value._name, value.id)
1201 elif isinstance(value, (int, long)):
1202 field_id = values.get('fields_id')
1203@@ -132,7 +131,7 @@
1204 return record.value_binary
1205 elif record.type == 'many2one':
1206 if not record.value_reference:
1207- return browse_null()
1208+ return False
1209 model, resource_id = record.value_reference.split(',')
1210 return self.pool.get(model).browse(cr, uid, int(resource_id), context=context)
1211 elif record.type == 'datetime':
1212
1213=== modified file 'openerp/addons/base/res/res_company.py'
1214--- openerp/addons/base/res/res_company.py 2013-12-09 15:14:54 +0000
1215+++ openerp/addons/base/res/res_company.py 2014-01-22 16:19:54 +0000
1216@@ -84,7 +84,7 @@
1217 if company.partner_id:
1218 address_data = part_obj.address_get(cr, openerp.SUPERUSER_ID, [company.partner_id.id], adr_pref=['default'])
1219 if address_data['default']:
1220- address = part_obj.read(cr, openerp.SUPERUSER_ID, address_data['default'], field_names, context=context)
1221+ address = part_obj.read(cr, openerp.SUPERUSER_ID, [address_data['default']], field_names, context=context)[0]
1222 for field in field_names:
1223 result[company.id][field] = address[field] or False
1224 return result
1225@@ -175,6 +175,7 @@
1226 res += '\n%s: %s' % (title, ', '.join(name for id, name in account_names))
1227
1228 return {'value': {'rml_footer': res, 'rml_footer_readonly': res}}
1229+
1230 def onchange_state(self, cr, uid, ids, state_id, context=None):
1231 if state_id:
1232 return {'value':{'country_id': self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id }}
1233
1234=== modified file 'openerp/addons/base/res/res_config.py'
1235--- openerp/addons/base/res/res_config.py 2013-12-02 11:00:30 +0000
1236+++ openerp/addons/base/res/res_config.py 2014-01-22 16:19:54 +0000
1237@@ -293,10 +293,10 @@
1238 def _already_installed(self, cr, uid, context=None):
1239 """ For each module (boolean fields in a res.config.installer),
1240 check if it's already installed (either 'to install', 'to upgrade'
1241- or 'installed') and if it is return the module's browse_record
1242+ or 'installed') and if it is return the module's record
1243
1244 :returns: a list of all installed modules in this installer
1245- :rtype: [browse_record]
1246+ :rtype: recordset (collection of Record)
1247 """
1248 modules = self.pool['ir.module.module']
1249
1250
1251=== modified file 'openerp/addons/base/res/res_currency.py'
1252--- openerp/addons/base/res/res_currency.py 2013-11-04 13:46:18 +0000
1253+++ openerp/addons/base/res/res_currency.py 2014-01-22 16:19:54 +0000
1254@@ -22,6 +22,7 @@
1255 import re
1256 import time
1257
1258+from openerp import api, fields as fields2
1259 from openerp import tools
1260 from openerp.osv import fields, osv
1261 from openerp.tools import float_round, float_is_zero, float_compare
1262@@ -77,7 +78,6 @@
1263 'rounding': fields.float('Rounding Factor', digits=(12,6)),
1264 'active': fields.boolean('Active'),
1265 'company_id':fields.many2one('res.company', 'Company'),
1266- 'date': fields.date('Date'),
1267 'base': fields.boolean('Base'),
1268 'position': fields.selection([('after','After Amount'),('before','Before Amount')], 'Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
1269 }
1270@@ -109,19 +109,12 @@
1271 ON res_currency
1272 (name, (COALESCE(company_id,-1)))""")
1273
1274- def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
1275- res = super(res_currency, self).read(cr, user, ids, fields, context, load)
1276- currency_rate_obj = self.pool.get('res.currency.rate')
1277- values = res
1278- if not isinstance(values, list):
1279- values = [values]
1280- for r in values:
1281- if r.__contains__('rate_ids'):
1282- rates=r['rate_ids']
1283- if rates:
1284- currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
1285- r['date'] = currency_date
1286- return res
1287+ date = fields2.Date(compute='compute_date', store=True)
1288+
1289+ @api.one
1290+ @api.depends('rate_ids.name')
1291+ def compute_date(self):
1292+ self.date = self.rate_ids.name
1293
1294 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
1295 if not args:
1296@@ -147,7 +140,7 @@
1297 """Return ``amount`` rounded according to ``currency``'s
1298 rounding rules.
1299
1300- :param browse_record currency: currency for which we are rounding
1301+ :param Record currency: currency for which we are rounding
1302 :param float amount: the amount to round
1303 :return: rounded float
1304 """
1305@@ -165,7 +158,7 @@
1306 they respectively round to 0.01 and 0.0, even though
1307 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
1308
1309- :param browse_record currency: currency for which we are rounding
1310+ :param Record currency: currency for which we are rounding
1311 :param float amount1: first amount to compare
1312 :param float amount2: second amount to compare
1313 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
1314@@ -183,7 +176,7 @@
1315 computing the difference, while the latter will round before, giving
1316 different results for e.g. 0.006 and 0.002 at 2 digits precision.
1317
1318- :param browse_record currency: currency for which we are rounding
1319+ :param Record currency: currency for which we are rounding
1320 :param float amount: amount to compare with currency's zero
1321 """
1322 return float_is_zero(amount, precision_rounding=currency.rounding)
1323@@ -255,7 +248,7 @@
1324 'currency_rate_type_id': fields.many2one('res.currency.rate.type', 'Currency Rate Type', help="Allow you to define your own currency rate types, like 'Average' or 'Year to Date'. Leave empty if you simply want to use the normal 'spot' rate type"),
1325 }
1326 _defaults = {
1327- 'name': lambda *a: time.strftime('%Y-%m-%d'),
1328+ 'name': lambda *a: time.strftime('%Y-%m-%d 00:00:00'),
1329 }
1330 _order = "name desc"
1331
1332
1333=== modified file 'openerp/addons/base/res/res_partner.py'
1334--- openerp/addons/base/res/res_partner.py 2013-09-10 12:12:30 +0000
1335+++ openerp/addons/base/res/res_partner.py 2014-01-22 16:19:54 +0000
1336@@ -29,41 +29,44 @@
1337 from openerp import SUPERUSER_ID
1338 from openerp import tools
1339 from openerp.osv import osv, fields
1340+from openerp.osv.scope import proxy as scope
1341+from openerp.osv.api import model, multi, one, returns
1342 from openerp.tools.translate import _
1343 from openerp.tools.yaml_import import is_comment
1344
1345+ADDRESS_FORMAT_LAYOUTS = {
1346+ '%(city)s %(state_code)s\n%(zip)s': """
1347+ <div class="address_format">
1348+ <field name="city" placeholder="City" style="width: 50%%"/>
1349+ <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/>
1350+ <br/>
1351+ <field name="zip" placeholder="ZIP"/>
1352+ </div>
1353+ """,
1354+ '%(zip)s %(city)s': """
1355+ <div class="address_format">
1356+ <field name="zip" placeholder="ZIP" style="width: 40%%"/>
1357+ <field name="city" placeholder="City" style="width: 57%%"/>
1358+ <br/>
1359+ <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
1360+ </div>
1361+ """,
1362+ '%(city)s\n%(state_name)s\n%(zip)s': """
1363+ <div class="address_format">
1364+ <field name="city" placeholder="City"/>
1365+ <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
1366+ <field name="zip" placeholder="ZIP"/>
1367+ </div>
1368+ """
1369+}
1370+
1371+
1372 class format_address(object):
1373- def fields_view_get_address(self, cr, uid, arch, context={}):
1374- user_obj = self.pool['res.users']
1375- fmt = user_obj.browse(cr, SUPERUSER_ID, uid, context).company_id.country_id
1376- fmt = fmt and fmt.address_format
1377- layouts = {
1378- '%(city)s %(state_code)s\n%(zip)s': """
1379- <div class="address_format">
1380- <field name="city" placeholder="City" style="width: 50%%"/>
1381- <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/>
1382- <br/>
1383- <field name="zip" placeholder="ZIP"/>
1384- </div>
1385- """,
1386- '%(zip)s %(city)s': """
1387- <div class="address_format">
1388- <field name="zip" placeholder="ZIP" style="width: 40%%"/>
1389- <field name="city" placeholder="City" style="width: 57%%"/>
1390- <br/>
1391- <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
1392- </div>
1393- """,
1394- '%(city)s\n%(state_name)s\n%(zip)s': """
1395- <div class="address_format">
1396- <field name="city" placeholder="City"/>
1397- <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
1398- <field name="zip" placeholder="ZIP"/>
1399- </div>
1400- """
1401- }
1402- for k,v in layouts.items():
1403- if fmt and (k in fmt):
1404+ @model
1405+ def fields_view_get_address(self, arch):
1406+ fmt = scope.user.company_id.country_id.address_format or ''
1407+ for k, v in ADDRESS_FORMAT_LAYOUTS.items():
1408+ if k in fmt:
1409 doc = etree.fromstring(arch)
1410 for node in doc.xpath("//div[@class='address_format']"):
1411 tree = etree.fromstring(v)
1412@@ -73,53 +76,53 @@
1413 return arch
1414
1415
1416-def _tz_get(self,cr,uid, context=None):
1417+@model
1418+def _tz_get(self):
1419 # put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728
1420 return [(tz,tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')]
1421
1422-class res_partner_category(osv.osv):
1423+
1424+class res_partner_category(osv.Model):
1425
1426 def name_get(self, cr, uid, ids, context=None):
1427- """Return the categories' display name, including their direct
1428- parent by default.
1429+ """ Return the categories' display name, including their direct
1430+ parent by default.
1431
1432- :param dict context: the ``partner_category_display`` key can be
1433- used to select the short version of the
1434- category name (without the direct parent),
1435- when set to ``'short'``. The default is
1436- the long version."""
1437+ If ``context['partner_category_display']`` is ``'short'``, the short
1438+ version of the category name (without the direct parent) is used.
1439+ The default is the long version.
1440+ """
1441+ if not isinstance(ids, list):
1442+ ids = [ids]
1443 if context is None:
1444 context = {}
1445+
1446 if context.get('partner_category_display') == 'short':
1447 return super(res_partner_category, self).name_get(cr, uid, ids, context=context)
1448- if isinstance(ids, (int, long)):
1449- ids = [ids]
1450- reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context)
1451+
1452 res = []
1453- for record in reads:
1454- name = record['name']
1455- if record['parent_id']:
1456- name = record['parent_id'][1] + ' / ' + name
1457- res.append((record['id'], name))
1458+ for category in self.browse(cr, uid, ids, context=context):
1459+ names = []
1460+ current = category
1461+ while current:
1462+ names.append(current.name)
1463+ current = current.parent_id
1464+ res.append((category.id, ' / '.join(reversed(names))))
1465 return res
1466
1467- def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
1468- if not args:
1469- args = []
1470- if not context:
1471- context = {}
1472+ @model
1473+ def name_search(self, name, args=None, operator='ilike', limit=100):
1474+ args = args or []
1475 if name:
1476 # Be sure name_search is symetric to name_get
1477 name = name.split(' / ')[-1]
1478- ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
1479- else:
1480- ids = self.search(cr, uid, args, limit=limit, context=context)
1481- return self.name_get(cr, uid, ids, context)
1482-
1483-
1484- def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1485- res = self.name_get(cr, uid, ids, context=context)
1486- return dict(res)
1487+ args = [('name', operator, name)] + args
1488+ categories = self.search(args, limit=limit)
1489+ return categories.name_get()
1490+
1491+ @multi
1492+ def _name_get_fnc(self, field_name, arg):
1493+ return dict(self.name_get())
1494
1495 _description = 'Partner Tags'
1496 _name = 'res.partner.category'
1497@@ -143,6 +146,7 @@
1498 _parent_order = 'name'
1499 _order = 'parent_left'
1500
1501+
1502 class res_partner_title(osv.osv):
1503 _name = 'res.partner.title'
1504 _order = 'name'
1505@@ -155,16 +159,17 @@
1506 'domain': 'contact',
1507 }
1508
1509-def _lang_get(self, cr, uid, context=None):
1510- lang_pool = self.pool['res.lang']
1511- ids = lang_pool.search(cr, uid, [], context=context)
1512- res = lang_pool.read(cr, uid, ids, ['code', 'name'], context)
1513- return [(r['code'], r['name']) for r in res]
1514+
1515+@model
1516+def _lang_get(self):
1517+ languages = scope['res.lang'].search([])
1518+ return [(language.code, language.name) for language in languages]
1519
1520 # fields copy if 'use_parent_address' is checked
1521 ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id')
1522
1523-class res_partner(osv.osv, format_address):
1524+
1525+class res_partner(osv.Model, format_address):
1526 _description = 'Partner'
1527 _name = "res.partner"
1528
1529@@ -174,26 +179,23 @@
1530 res[partner.id] = self._display_address(cr, uid, partner, context=context)
1531 return res
1532
1533- def _get_image(self, cr, uid, ids, name, args, context=None):
1534- result = dict.fromkeys(ids, False)
1535- for obj in self.browse(cr, uid, ids, context=context):
1536- result[obj.id] = tools.image_get_resized_images(obj.image)
1537- return result
1538-
1539- def _get_tz_offset(self, cr, uid, ids, name, args, context=None):
1540- result = dict.fromkeys(ids, False)
1541- for obj in self.browse(cr, uid, ids, context=context):
1542- result[obj.id] = datetime.datetime.now(pytz.timezone(obj.tz or 'GMT')).strftime('%z')
1543- return result
1544-
1545- def _set_image(self, cr, uid, id, name, value, args, context=None):
1546- return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1547-
1548- def _has_image(self, cr, uid, ids, name, args, context=None):
1549- result = {}
1550- for obj in self.browse(cr, uid, ids, context=context):
1551- result[obj.id] = obj.image != False
1552- return result
1553+ @multi
1554+ def _get_tz_offset(self, name, args):
1555+ return dict(
1556+ (p.id, datetime.datetime.now(pytz.timezone(p.tz or 'GMT')).strftime('%z'))
1557+ for p in self)
1558+
1559+ @multi
1560+ def _get_image(self, name, args):
1561+ return dict((p.id, tools.image_get_resized_images(p.image)) for p in self)
1562+
1563+ @one
1564+ def _set_image(self, name, value, args):
1565+ return self.write({'image': tools.image_resize_image_big(value)})
1566+
1567+ @multi
1568+ def _has_image(self, name, args):
1569+ return dict((p.id, bool(p.image)) for p in self)
1570
1571 def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):
1572 """ Returns the partner that is considered the commercial
1573@@ -302,16 +304,15 @@
1574 'commercial_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers)
1575 }
1576
1577- def _default_category(self, cr, uid, context=None):
1578- if context is None:
1579- context = {}
1580- if context.get('category_id'):
1581- return [context['category_id']]
1582- return False
1583+ @model
1584+ def _default_category(self):
1585+ category_id = scope.context.get('category_id', False)
1586+ return [category_id] if category_id else False
1587
1588- def _get_default_image(self, cr, uid, is_company, context=None, colorize=False):
1589- img_path = openerp.modules.get_module_resource('base', 'static/src/img',
1590- ('company_image.png' if is_company else 'avatar.png'))
1591+ @model
1592+ def _get_default_image(self, is_company, colorize=False):
1593+ img_path = openerp.modules.get_module_resource(
1594+ 'base', 'static/src/img', 'company_image.png' if is_company else 'avatar.png')
1595 with open(img_path, 'rb') as f:
1596 image = f.read()
1597
1598@@ -329,13 +330,17 @@
1599 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
1600 return res
1601
1602+ @model
1603+ def _default_company(self):
1604+ return scope['res.company']._company_default_get('res.partner')
1605+
1606 _defaults = {
1607 'active': True,
1608- 'lang': lambda self, cr, uid, ctx: ctx.get('lang', 'en_US'),
1609- 'tz': lambda self, cr, uid, ctx: ctx.get('tz', False),
1610+ 'lang': model(lambda self: scope.lang),
1611+ 'tz': model(lambda self: scope.context.get('tz', False)),
1612 'customer': True,
1613 'category_id': _default_category,
1614- 'company_id': lambda self, cr, uid, ctx: self.pool['res.company']._company_default_get(cr, uid, 'res.partner', context=ctx),
1615+ 'company_id': _default_company,
1616 'color': 0,
1617 'is_company': False,
1618 'type': 'contact', # type 'default' is wildcard and thus inappropriate
1619@@ -347,14 +352,13 @@
1620 (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']),
1621 ]
1622
1623- def copy(self, cr, uid, id, default=None, context=None):
1624- if default is None:
1625- default = {}
1626- name = self.read(cr, uid, [id], ['name'], context)[0]['name']
1627- default.update({'name': _('%s (copy)') % name})
1628- return super(res_partner, self).copy(cr, uid, id, default, context)
1629+ @one
1630+ def copy(self, default=None):
1631+ default = dict(default or {}, name=_('%s (copy)') % self.name)
1632+ return super(res_partner, self).copy(default)
1633
1634- def onchange_type(self, cr, uid, ids, is_company, context=None):
1635+ @multi
1636+ def onchange_type(self, is_company):
1637 value = {}
1638 value['title'] = False
1639 if is_company:
1640@@ -384,10 +388,11 @@
1641 result['value'] = {'use_parent_address': False}
1642 return result
1643
1644- def onchange_state(self, cr, uid, ids, state_id, context=None):
1645+ @multi
1646+ def onchange_state(self, state_id):
1647 if state_id:
1648- country_id = self.pool['res.country.state'].browse(cr, uid, state_id, context).country_id.id
1649- return {'value':{'country_id':country_id}}
1650+ state = scope['res.country.state'].browse(state_id)
1651+ return {'value': {'country_id': state.country_id.id}}
1652 return {}
1653
1654 def _check_ean_key(self, cr, uid, ids, context=None):
1655@@ -505,30 +510,32 @@
1656 if not parent.is_company:
1657 parent.write({'is_company': True})
1658
1659- def write(self, cr, uid, ids, vals, context=None):
1660- if isinstance(ids, (int, long)):
1661- ids = [ids]
1662- #res.partner must only allow to set the company_id of a partner if it
1663- #is the same as the company of all users that inherit from this partner
1664- #(this is to allow the code from res_users to write to the partner!) or
1665- #if setting the company_id to False (this is compatible with any user company)
1666+ @multi
1667+ def write(self, vals):
1668+ # res.partner must only allow to set the company_id of a partner if it
1669+ # is the same as the company of all users that inherit from this partner
1670+ # (this is to allow the code from res_users to write to the partner!) or
1671+ # if setting the company_id to False (this is compatible with any user
1672+ # company)
1673 if vals.get('company_id'):
1674- for partner in self.browse(cr, uid, ids, context=context):
1675+ company = self.pool['res.company'].browse(vals['company_id'])
1676+ for partner in self:
1677 if partner.user_ids:
1678- user_companies = set([user.company_id.id for user in partner.user_ids])
1679- if len(user_companies) > 1 or vals['company_id'] not in user_companies:
1680+ companies = set(user.company_id for user in partner.user_ids)
1681+ if len(companies) > 1 or company not in companies:
1682 raise osv.except_osv(_("Warning"),_("You can not change the company as the partner/user has multiple user linked with different companies."))
1683- result = super(res_partner,self).write(cr, uid, ids, vals, context=context)
1684- for partner in self.browse(cr, uid, ids, context=context):
1685- self._fields_sync(cr, uid, partner, vals, context)
1686+
1687+ result = super(res_partner, self).write(vals)
1688+ for partner in self:
1689+ self._fields_sync(partner, vals)
1690 return result
1691
1692- def create(self, cr, uid, vals, context=None):
1693- new_id = super(res_partner, self).create(cr, uid, vals, context=context)
1694- partner = self.browse(cr, uid, new_id, context=context)
1695- self._fields_sync(cr, uid, partner, vals, context)
1696- self._handle_first_contact_creation(cr, uid, partner, context)
1697- return new_id
1698+ @model
1699+ def create(self, vals):
1700+ partner = super(res_partner, self).create(vals)
1701+ self._fields_sync(partner, vals)
1702+ self._handle_first_contact_creation(partner)
1703+ return partner
1704
1705 def open_commercial_entity(self, cr, uid, ids, context=None):
1706 """ Utility method used to add an "Open Company" button in partner views """
1707@@ -712,14 +719,11 @@
1708 return False
1709 return _('Partners: ')+self.pool['res.partner.category'].browse(cr, uid, context['category_id'], context).name
1710
1711- def main_partner(self, cr, uid):
1712- ''' Return the id of the main partner
1713- '''
1714- model_data = self.pool['ir.model.data']
1715- return model_data.browse(cr, uid,
1716- model_data.search(cr, uid, [('module','=','base'),
1717- ('name','=','main_partner')])[0],
1718- ).res_id
1719+ @model
1720+ @returns('self')
1721+ def main_partner(self):
1722+ ''' Return the main partner '''
1723+ return scope.ref('base.main_partner')
1724
1725 def _display_address(self, cr, uid, address, without_company=False, context=None):
1726
1727@@ -735,14 +739,14 @@
1728
1729 # get the information that will be injected into the display format
1730 # get the address format
1731- address_format = address.country_id and address.country_id.address_format or \
1732+ address_format = address.country_id.address_format or \
1733 "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"
1734 args = {
1735- 'state_code': address.state_id and address.state_id.code or '',
1736- 'state_name': address.state_id and address.state_id.name or '',
1737- 'country_code': address.country_id and address.country_id.code or '',
1738- 'country_name': address.country_id and address.country_id.name or '',
1739- 'company_name': address.parent_id and address.parent_id.name or '',
1740+ 'state_code': address.state_id.code or '',
1741+ 'state_name': address.state_id.name or '',
1742+ 'country_code': address.country_id.code or '',
1743+ 'country_name': address.country_id.name or '',
1744+ 'company_name': address.parent_id.name or '',
1745 }
1746 for field in self._address_fields(cr, uid, context=context):
1747 args[field] = getattr(address, field) or ''
1748
1749=== modified file 'openerp/addons/base/res/res_users.py'
1750--- openerp/addons/base/res/res_users.py 2014-01-15 20:53:57 +0000
1751+++ openerp/addons/base/res/res_users.py 2014-01-22 16:19:54 +0000
1752@@ -25,11 +25,10 @@
1753 from lxml.builder import E
1754
1755 import openerp
1756-from openerp import SUPERUSER_ID
1757+from openerp import SUPERUSER_ID, BaseModel
1758 from openerp import tools
1759 import openerp.exceptions
1760-from openerp.osv import fields,osv
1761-from openerp.osv.orm import browse_record
1762+from openerp.osv import fields, osv, api
1763 from openerp.tools.translate import _
1764
1765 _logger = logging.getLogger(__name__)
1766@@ -206,9 +205,8 @@
1767 def _get_company(self,cr, uid, context=None, uid2=False):
1768 if not uid2:
1769 uid2 = uid
1770- user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)
1771- company_id = user.get('company_id', False)
1772- return company_id and company_id[0] or False
1773+ user = self.pool['res.users'].browse(cr, uid, uid2, context)
1774+ return user.company_id.id
1775
1776 def _get_companies(self, cr, uid, context=None):
1777 c = self._get_company(cr, uid, context)
1778@@ -247,7 +245,7 @@
1779 'company_id': _get_company,
1780 'company_ids': _get_companies,
1781 'groups_id': _get_group,
1782- 'image': lambda self, cr, uid, ctx={}: self.pool['res.partner']._get_default_image(cr, uid, False, ctx, colorize=True),
1783+ 'image': api.model(lambda self: self.pool['res.partner']._get_default_image(False, colorize=True)),
1784 }
1785
1786 # User can write on a few of his own fields (but not his groups for example)
1787@@ -295,7 +293,8 @@
1788 break
1789 else:
1790 if 'company_id' in values:
1791- if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
1792+ user = self.browse(cr, SUPERUSER_ID, uid, context=context)
1793+ if not (values['company_id'] in user.company_ids.unbrowse()):
1794 del values['company_id']
1795 uid = 1 # safe fields only, so we write as super-user to bypass access rights
1796
1797@@ -360,8 +359,8 @@
1798 else:
1799 context_key = False
1800 if context_key:
1801- res = getattr(user,k) or False
1802- if isinstance(res, browse_record):
1803+ res = getattr(user, k) or False
1804+ if isinstance(res, BaseModel):
1805 res = res.id
1806 result[context_key] = res or False
1807 return result
1808@@ -383,7 +382,7 @@
1809 if not res:
1810 raise openerp.exceptions.AccessDenied()
1811
1812- def login(self, db, login, password):
1813+ def _login(self, db, login, password):
1814 if not password:
1815 return False
1816 user_id = False
1817@@ -412,6 +411,7 @@
1818 try:
1819 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
1820 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
1821+ self.invalidate_cache(['login_date'], [user_id])
1822 except Exception:
1823 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
1824 except openerp.exceptions.AccessDenied:
1825@@ -433,7 +433,7 @@
1826 :param dict user_agent_env: environment dictionary describing any
1827 relevant environment attributes
1828 """
1829- uid = self.login(db, login, password)
1830+ uid = self._login(db, login, password)
1831 if uid == openerp.SUPERUSER_ID:
1832 # Successfully logged in as admin!
1833 # Attempt to guess the web base url...
1834@@ -710,7 +710,7 @@
1835 def get_user_groups_view(self, cr, uid, context=None):
1836 try:
1837 view = self.pool['ir.model.data'].get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
1838- assert view and view._table_name == 'ir.ui.view'
1839+ assert view and view._name == 'ir.ui.view'
1840 except Exception:
1841 view = False
1842 return view
1843
1844=== modified file 'openerp/addons/base/security/base_security.xml'
1845--- openerp/addons/base/security/base_security.xml 2013-06-10 15:41:39 +0000
1846+++ openerp/addons/base/security/base_security.xml 2014-01-22 16:19:54 +0000
1847@@ -41,11 +41,6 @@
1848 <field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>
1849 </record>
1850
1851- <!-- Set accesses to menu -->
1852- <record model="ir.ui.menu" id="base.menu_administration">
1853- <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
1854- </record>
1855-
1856 <record model="ir.rule" id="res_partner_rule">
1857 <field name="name">res.partner company</field>
1858 <field name="model_id" ref="model_res_partner"/>
1859
1860=== modified file 'openerp/addons/base/test/base_test.yml'
1861--- openerp/addons/base/test/base_test.yml 2013-03-20 13:22:38 +0000
1862+++ openerp/addons/base/test/base_test.yml 2014-01-22 16:19:54 +0000
1863@@ -277,7 +277,7 @@
1864 rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01',
1865 'rate': value,
1866 'currency_id': currency.id})
1867- rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate']
1868+ rate = res_currency_rate.read(cr, 1, [rate_id], ['rate'])[0]['rate']
1869 assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected)
1870 # res.currency.rate uses 6 digits of precision by default
1871 try_roundtrip(2.6748955, 2.674896)
1872
1873=== modified file 'openerp/addons/base/test/test_ir_rule.yml'
1874--- openerp/addons/base/test/test_ir_rule.yml 2011-06-01 14:39:21 +0000
1875+++ openerp/addons/base/test/test_ir_rule.yml 2014-01-22 16:19:54 +0000
1876@@ -124,7 +124,7 @@
1877 Modify the global rule on res_company which triggers a recursive check
1878 of the rules on company.
1879 -
1880- !record {model: ir.rule, id: base.res_company_rule}:
1881+ !record {model: ir.rule, id: res_company_rule}:
1882 domain_force: "[('id','child_of',[user.company_id.id])]"
1883 -
1884 Read as demo user the partners (exercising the global company rule).
1885
1886=== modified file 'openerp/addons/base/test/test_osv_expression.yml'
1887--- openerp/addons/base/test/test_osv_expression.yml 2014-01-08 15:27:22 +0000
1888+++ openerp/addons/base/test/test_osv_expression.yml 2014-01-22 16:19:54 +0000
1889@@ -83,7 +83,7 @@
1890 Test one2many operator with False
1891 -
1892 !assert {model: res.partner, search: "[('child_ids', '=', False)]"}:
1893- - child_ids in (False, None, [])
1894+ - list(child_ids) == []
1895 -
1896 Test many2many operator with empty search list
1897 -
1898@@ -92,7 +92,7 @@
1899 Test many2many operator with False
1900 -
1901 !assert {model: res.partner, search: "[('category_id', '=', False)]"}:
1902- - category_id in (False, None, [])
1903+ - list(category_id) == []
1904 -
1905 Filtering on invalid value across x2many relationship should return an empty set
1906 -
1907
1908=== modified file 'openerp/cli/server.py'
1909--- openerp/cli/server.py 2014-01-09 09:32:58 +0000
1910+++ openerp/cli/server.py 2014-01-22 16:19:54 +0000
1911@@ -96,6 +96,7 @@
1912 registry = openerp.modules.registry.RegistryManager.new(dbname, update_module=update_module)
1913 except Exception:
1914 _logger.exception('Failed to initialize database `%s`.', dbname)
1915+ openerp.tools.post_mortem()
1916 return False
1917 return registry._assertion_report.failures == 0
1918
1919
1920=== modified file 'openerp/exceptions.py'
1921--- openerp/exceptions.py 2013-02-15 14:35:03 +0000
1922+++ openerp/exceptions.py 2014-01-22 16:19:54 +0000
1923@@ -28,6 +28,13 @@
1924 If you consider introducing new exceptions, check out the test_exceptions addon.
1925 """
1926
1927+# kept for backward compatibility
1928+class except_orm(Exception):
1929+ def __init__(self, name, value):
1930+ self.name = name
1931+ self.value = value
1932+ self.args = (name, value)
1933+
1934 class Warning(Exception):
1935 pass
1936
1937@@ -47,8 +54,15 @@
1938 super(AccessDenied, self).__init__('Access denied.')
1939 self.traceback = ('', '', '')
1940
1941-class AccessError(Exception):
1942+class AccessError(except_orm):
1943 """ Access rights error. """
1944+ def __init__(self, msg):
1945+ super(AccessError, self).__init__('AccessError', msg)
1946+
1947+class MissingError(except_orm):
1948+ """ Missing record(s). """
1949+ def __init__(self, msg):
1950+ super(MissingError, self).__init__('MissingError', msg)
1951
1952 class DeferredException(Exception):
1953 """ Exception object holding a traceback for asynchronous reporting.
1954
1955=== modified file 'openerp/modules/loading.py'
1956--- openerp/modules/loading.py 2013-12-18 12:38:58 +0000
1957+++ openerp/modules/loading.py 2014-01-22 16:19:54 +0000
1958@@ -175,6 +175,7 @@
1959 status['progress'] = (index + 0.75) / len(graph)
1960 _load_data(cr, module_name, idref, mode, kind='demo')
1961 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
1962+ modobj.invalidate_cache(['demo'], [module_id])
1963
1964 migrations.migrate_module(package, 'post')
1965
1966@@ -310,6 +311,7 @@
1967 modobj.button_upgrade(cr, SUPERUSER_ID, ids)
1968
1969 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
1970+ modobj.invalidate_cache(['state'])
1971
1972
1973 # STEP 3: Load marked modules (skipping base which was done in STEP 1)
1974
1975=== modified file 'openerp/modules/module.py'
1976--- openerp/modules/module.py 2013-06-28 15:07:55 +0000
1977+++ openerp/modules/module.py 2014-01-22 16:19:54 +0000
1978@@ -28,6 +28,7 @@
1979 import types
1980 import zipimport
1981
1982+import openerp
1983 import openerp.tools as tools
1984 import openerp.tools.osutil as osutil
1985 from openerp.tools.safe_eval import safe_eval as eval
1986@@ -310,22 +311,23 @@
1987 TODO better explanation of _auto_init and init.
1988
1989 """
1990- _logger.info('module %s: creating or updating database tables', module_name)
1991- todo = []
1992- for obj in obj_list:
1993- result = obj._auto_init(cr, {'module': module_name})
1994- if result:
1995- todo += result
1996- if hasattr(obj, 'init'):
1997- obj.init(cr)
1998- cr.commit()
1999- for obj in obj_list:
2000- obj._auto_end(cr, {'module': module_name})
2001- cr.commit()
2002- todo.sort()
2003- for t in todo:
2004- t[1](cr, *t[2])
2005- cr.commit()
2006+ with openerp.osv.scope.Scope(cr, openerp.SUPERUSER_ID, None):
2007+ _logger.info('module %s: creating or updating database tables', module_name)
2008+ todo = []
2009+ for obj in obj_list:
2010+ result = obj._auto_init(cr, {'module': module_name})
2011+ if result:
2012+ todo += result
2013+ if hasattr(obj, 'init'):
2014+ obj.init(cr)
2015+ cr.commit()
2016+ for obj in obj_list:
2017+ obj._auto_end(cr, {'module': module_name})
2018+ cr.commit()
2019+ todo.sort(key=lambda x: x[0])
2020+ for t in todo:
2021+ t[1](cr, *t[2])
2022+ cr.commit()
2023
2024 def load_openerp_module(module_name):
2025 """ Load an OpenERP module, if not already loaded.
2026
2027=== modified file 'openerp/modules/registry.py'
2028--- openerp/modules/registry.py 2013-11-20 10:25:45 +0000
2029+++ openerp/modules/registry.py 2014-01-22 16:19:54 +0000
2030@@ -27,12 +27,13 @@
2031 import logging
2032 import threading
2033
2034+from openerp import SUPERUSER_ID
2035 import openerp.sql_db
2036 import openerp.osv.orm
2037 import openerp.tools
2038 import openerp.modules.db
2039 import openerp.tools.config
2040-from openerp.tools import assertion_report
2041+from openerp.tools import assertion_report, lazy_property
2042
2043 _logger = logging.getLogger(__name__)
2044
2045@@ -49,6 +50,7 @@
2046 self.models = {} # model name/model instance mapping
2047 self._sql_error = {}
2048 self._store_function = {}
2049+ self._pure_function_fields = {} # {model: [field, ...], ...}
2050 self._init = True
2051 self._init_parent = {}
2052 self._assertion_report = assertion_report.assertion_report()
2053@@ -94,14 +96,20 @@
2054 """ Return an iterator over all model names. """
2055 return iter(self.models)
2056
2057- def __contains__(self, model_name):
2058- """ Test whether the model with the given name exists. """
2059- return model_name in self.models
2060-
2061 def __getitem__(self, model_name):
2062 """ Return the model with the given name or raise KeyError if it doesn't exist."""
2063 return self.models[model_name]
2064
2065+ @lazy_property
2066+ def pure_function_fields(self):
2067+ """ Return the list of pure function fields (field objects) """
2068+ fields = []
2069+ for mname, fnames in self._pure_function_fields.iteritems():
2070+ model_fields = self[mname]._fields
2071+ for fname in fnames:
2072+ fields.append(model_fields[fname])
2073+ return fields
2074+
2075 def do_parent_store(self, cr):
2076 for o in self._init_parent:
2077 self.get(o)._parent_store_compute(cr)
2078@@ -125,14 +133,26 @@
2079
2080 """
2081 models_to_load = [] # need to preserve loading order
2082- # Instantiate registered classes (via the MetaModel automatic discovery
2083- # or via explicit constructor call), and add them to the pool.
2084- for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
2085- # models register themselves in self.models
2086- model = cls.create_instance(self, cr)
2087- if model._name not in models_to_load:
2088- # avoid double-loading models whose declaration is split
2089- models_to_load.append(model._name)
2090+ lazy_property.reset_all(self)
2091+
2092+ with openerp.osv.scope.Scope(cr, SUPERUSER_ID, None):
2093+ # call hook before adding stuff in the registry
2094+ for model in self.models.itervalues():
2095+ model._before_registry_update()
2096+
2097+ # Instantiate registered classes (via the MetaModel automatic discovery
2098+ # or via explicit constructor call), and add them to the pool.
2099+ for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
2100+ # models register themselves in self.models
2101+ model = cls._build_model(self, cr)
2102+ if model._name not in models_to_load:
2103+ # avoid double-loading models whose declaration is split
2104+ models_to_load.append(model._name)
2105+
2106+ # call hook after models have been instantiated
2107+ for model in self.models.itervalues():
2108+ model._after_registry_update()
2109+
2110 return [self.models[m] for m in models_to_load]
2111
2112 def clear_caches(self):
2113@@ -144,7 +164,7 @@
2114 model.clear_caches()
2115 # Special case for ir_ui_menu which does not use openerp.tools.ormcache.
2116 ir_ui_menu = self.models.get('ir.ui.menu')
2117- if ir_ui_menu:
2118+ if ir_ui_menu is not None:
2119 ir_ui_menu.clear_cache()
2120
2121
2122
2123=== modified file 'openerp/netsvc.py'
2124--- openerp/netsvc.py 2013-05-28 10:27:33 +0000
2125+++ openerp/netsvc.py 2014-01-22 16:19:54 +0000
2126@@ -258,16 +258,12 @@
2127 raise
2128 except openerp.exceptions.DeferredException, e:
2129 _logger.exception(tools.exception_to_unicode(e))
2130- post_mortem(e.traceback)
2131+ tools.post_mortem(e.traceback)
2132 raise
2133 except Exception, e:
2134 _logger.exception(tools.exception_to_unicode(e))
2135- post_mortem(sys.exc_info())
2136+ tools.post_mortem()
2137 raise
2138
2139-def post_mortem(info):
2140- if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType):
2141- import pdb
2142- pdb.post_mortem(info[2])
2143
2144 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
2145
2146=== modified file 'openerp/osv/__init__.py'
2147--- openerp/osv/__init__.py 2013-02-12 14:24:10 +0000
2148+++ openerp/osv/__init__.py 2014-01-22 16:19:54 +0000
2149@@ -19,9 +19,10 @@
2150 #
2151 ##############################################################################
2152
2153+import api
2154 import osv
2155+import scope
2156 import fields
2157-
2158+import fields2
2159
2160 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
2161-
2162
2163=== added file 'openerp/osv/api.py'
2164--- openerp/osv/api.py 1970-01-01 00:00:00 +0000
2165+++ openerp/osv/api.py 2014-01-22 16:19:54 +0000
2166@@ -0,0 +1,760 @@
2167+# -*- coding: utf-8 -*-
2168+##############################################################################
2169+#
2170+# OpenERP, Open Source Management Solution
2171+# Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
2172+#
2173+# This program is free software: you can redistribute it and/or modify
2174+# it under the terms of the GNU Affero General Public License as
2175+# published by the Free Software Foundation, either version 3 of the
2176+# License, or (at your option) any later version.
2177+#
2178+# This program is distributed in the hope that it will be useful,
2179+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2180+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2181+# GNU Affero General Public License for more details.
2182+#
2183+# You should have received a copy of the GNU Affero General Public License
2184+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2185+#
2186+##############################################################################
2187+
2188+""" This module provides the elements for managing two different API styles,
2189+ namely the "traditional" and "record" styles.
2190+
2191+ In the "traditional" style, parameters like the database cursor, user id,
2192+ context dictionary and record ids (usually denoted as ``cr``, ``uid``,
2193+ ``context``, ``ids``) are passed explicitly to all methods. In the "record"
2194+ style, those parameters are hidden in an execution environment (a "scope")
2195+ and the methods only refer to model instances, which gives it a more object-
2196+ oriented feel.
2197+
2198+ For instance, the statements::
2199+
2200+ model = self.pool.get(MODEL)
2201+ ids = model.search(cr, uid, DOMAIN, context=context)
2202+ for rec in model.browse(cr, uid, ids, context=context):
2203+ print rec.name
2204+ model.write(cr, uid, ids, VALUES, context=context)
2205+
2206+ may also be written as::
2207+
2208+ with scope(cr, uid, context): # cr, uid, context introduced once
2209+ model = scope[MODEL] # scope proxies the current scope
2210+ recs = model.search(DOMAIN) # search returns a recordset
2211+ for rec in recs: # iterate over the records
2212+ print rec.name
2213+ recs.write(VALUES) # update all records in recs
2214+
2215+ Methods written in the "traditional" style are automatically decorated,
2216+ following some heuristics based on parameter names.
2217+"""
2218+
2219+__all__ = [
2220+ 'Meta', 'guess', 'noguess',
2221+ 'model', 'multi', 'one',
2222+ 'cr', 'cr_context', 'cr_uid', 'cr_uid_context',
2223+ 'cr_uid_id', 'cr_uid_id_context', 'cr_uid_ids', 'cr_uid_ids_context',
2224+ 'constrains', 'depends', 'returns',
2225+]
2226+
2227+from functools import update_wrapper
2228+from inspect import getargspec
2229+import logging
2230+
2231+_logger = logging.getLogger(__name__)
2232+
2233+
2234+#
2235+# The following attributes are used, and reflected on wrapping methods:
2236+# - method._api: decorator function, used for re-applying decorator
2237+# - method._constrains: set by @constrains, specifies constraint dependencies
2238+# - method._depends: set by @depends, specifies compute dependencies
2239+# - method._returns: set by @returns, specifies return model
2240+# - method.clear_cache: set by @ormcache, used to clear the cache
2241+#
2242+# On wrapping method only:
2243+# - method._orig: original method
2244+#
2245+
2246+_WRAPPED_ATTRS = ('_api', '_constrains', '_depends', '_returns', 'clear_cache')
2247+
2248+
2249+class Meta(type):
2250+ """ Metaclass that automatically decorates traditional-style methods by
2251+ guessing their API. It also implements the inheritance of the
2252+ :func:`returns` decorators.
2253+ """
2254+
2255+ def __new__(meta, name, bases, attrs):
2256+ # dummy parent class to catch overridden methods decorated with 'returns'
2257+ parent = type.__new__(meta, name, bases, {})
2258+
2259+ for key, value in attrs.items():
2260+ if not key.startswith('__') and callable(value):
2261+ # make the method inherit from @returns decorators
2262+ if not _returns(value) and _returns(getattr(parent, key, None)):
2263+ value = returns(getattr(parent, key))(value)
2264+ _logger.debug("Method %s.%s inherited @returns%r",
2265+ name, value.__name__, _returns(value))
2266+
2267+ # guess calling convention if none is given
2268+ if not hasattr(value, '_api'):
2269+ value = guess(value)
2270+
2271+ attrs[key] = value
2272+
2273+ return type.__new__(meta, name, bases, attrs)
2274+
2275+
2276+def constrains(*args):
2277+ """ Return a decorator that specifies the field dependencies of a method
2278+ implementing a constraint checker. Each argument must be a field name.
2279+ """
2280+ def decorate(method):
2281+ method._constrains = args
2282+ return method
2283+
2284+ return decorate
2285+
2286+
2287+def depends(*args):
2288+ """ Return a decorator that specifies the field dependencies of a "compute"
2289+ method (for new-style function fields). Each argument must be a string
2290+ that consists in a dot-separated sequence of field names.
2291+
2292+ One may also pass a single function as argument. In that case, the
2293+ dependencies are given by calling the function with the field's model.
2294+ """
2295+ if args and callable(args[0]):
2296+ args = args[0]
2297+
2298+ def decorate(method):
2299+ method._depends = args
2300+ return method
2301+
2302+ return decorate
2303+
2304+
2305+def returns(model, traditional=None):
2306+ """ Return a decorator for methods that return instances of `model`.
2307+
2308+ :param model: a model name, ``'self'`` for the current model, or a method
2309+ (in which case the model is taken from that method's decorator)
2310+
2311+ :param traditional: a function `convert(self, value)` to convert the
2312+ record-style value to the traditional-style output
2313+
2314+ The decorator adapts the method output to the api style: `id`, `ids` or
2315+ ``False`` for the traditional style, and record, recordset or null for
2316+ the record style::
2317+
2318+ @model
2319+ @returns('res.partner')
2320+ def find_partner(self, arg):
2321+ ... # return some record
2322+
2323+ # output depends on call style: traditional vs record style
2324+ partner_id = model.find_partner(cr, uid, arg, context=context)
2325+ partner_record = model.find_partner(arg)
2326+
2327+ Note that the decorated method must satisfy that convention.
2328+
2329+ Those decorators are automatically *inherited*: a method that overrides
2330+ a decorated existing method will be decorated with the same
2331+ ``@returns(model)``.
2332+ """
2333+ if callable(model):
2334+ # model is a method, check its own @returns decoration
2335+ spec = _returns(model)
2336+ if not spec:
2337+ return lambda method: method
2338+ else:
2339+ spec = model, traditional
2340+
2341+ def decorate(method):
2342+ if hasattr(method, '_orig'):
2343+ # decorate the original method, and re-apply the api decorator
2344+ origin = method._orig
2345+ origin._returns = spec
2346+ return origin._api(origin)
2347+ else:
2348+ method._returns = spec
2349+ return method
2350+
2351+ return decorate
2352+
2353+
2354+def _returns(method):
2355+ return getattr(method, '_returns', None)
2356+
2357+
2358+# constant converters
2359+_CONVERT_PASS = lambda self, value: value
2360+_CONVERT_BROWSE = lambda self, value: self.browse(value)
2361+_CONVERT_UNBROWSE = lambda self, value: value.unbrowse()
2362+
2363+
2364+def _converter_to_old(method):
2365+ """ Return a function `convert(self, value)` that adapts `value` from
2366+ record-style to traditional-style returning convention of `method`.
2367+ """
2368+ spec = _returns(method)
2369+ if spec:
2370+ model, traditional = spec
2371+ return traditional or _CONVERT_UNBROWSE
2372+ else:
2373+ return _CONVERT_PASS
2374+
2375+
2376+def _converter_to_new(method):
2377+ """ Return a function `convert(self, value)` that adapts `value` from
2378+ traditional-style to record-style returning convention of `method`.
2379+ """
2380+ spec = _returns(method)
2381+ if spec:
2382+ model, traditional = spec
2383+ if model == 'self':
2384+ return _CONVERT_BROWSE
2385+ else:
2386+ return lambda self, value: self.pool[model].browse(value)
2387+ else:
2388+ return _CONVERT_PASS
2389+
2390+
2391+def _aggregator_one(method):
2392+ """ Return a function `convert(self, value)` that aggregates record-style
2393+ `value` for a method decorated with ``@one``.
2394+ """
2395+ spec = _returns(method)
2396+ if spec:
2397+ # value is a list of instances, concatenate them
2398+ model, traditional = spec
2399+ if model == 'self':
2400+ return lambda self, value: sum(value, self.browse())
2401+ else:
2402+ return lambda self, value: sum(value, self.pool[model].browse())
2403+ else:
2404+ return _CONVERT_PASS
2405+
2406+
2407+def wraps(method):
2408+ """ Return a decorator for an api wrapper of `method`. """
2409+ def decorate(wrapper):
2410+ # propagate '__module__', '__name__', '__doc__' to wrapper
2411+ update_wrapper(wrapper, method)
2412+ # propagate specific openerp attributes to wrapper
2413+ for attr in _WRAPPED_ATTRS:
2414+ if hasattr(method, attr):
2415+ setattr(wrapper, attr, getattr(method, attr))
2416+ wrapper._orig = method
2417+ return wrapper
2418+
2419+ return decorate
2420+
2421+
2422+def _has_cursor(args, kwargs):
2423+ """ test whether `args` or `kwargs` contain a cursor argument """
2424+ cr = args[0] if args else kwargs.get('cr')
2425+ return isinstance(cr, Cursor)
2426+
2427+
2428+def _cr_uid_context_splitter(method):
2429+ """ return a function that splits scope parameters from the other arguments """
2430+ names = getargspec(method).args[1:]
2431+ ctx_pos = len(names) + 2
2432+
2433+ def split(args, kwargs):
2434+ cr, uid = args[:2]
2435+ if ctx_pos < len(args):
2436+ return cr, uid, args[ctx_pos], args[2:ctx_pos], kwargs
2437+ else:
2438+ return cr, uid, kwargs.pop('context', None), args[2:], kwargs
2439+
2440+ return split
2441+
2442+
2443+def _cr_uid_ids_context_splitter(method):
2444+ """ return a function that splits scope parameters from the other arguments """
2445+ names = getargspec(method).args[1:]
2446+ ctx_pos = len(names) + 3
2447+
2448+ def split(args, kwargs):
2449+ cr, uid, ids = args[:3]
2450+ if ctx_pos < len(args):
2451+ return cr, uid, ids, args[ctx_pos], args[3:ctx_pos], kwargs
2452+ else:
2453+ return cr, uid, ids, kwargs.pop('context', None), args[3:], kwargs
2454+
2455+ return split
2456+
2457+
2458+def _scope_cr_getter(method):
2459+ """ return a function that makes the scope corresponding to parameters """
2460+ names = getargspec(method).args
2461+ cr_name = len(names) > 1 and names[1]
2462+
2463+ def get(args, kwargs):
2464+ cr = args[0] if args else kwargs[cr_name]
2465+ return Scope(cr, SUPERUSER_ID, None)
2466+
2467+ return get
2468+
2469+
2470+def _scope_cr_uid_getter(method):
2471+ """ return a function that makes the scope corresponding to parameters """
2472+ names = getargspec(method).args
2473+ cr_name = len(names) > 1 and names[1]
2474+ uid_name = len(names) > 2 and names[2]
2475+
2476+ def get(args, kwargs):
2477+ nargs = len(args)
2478+ cr = args[0] if nargs > 0 else kwargs[cr_name]
2479+ uid = args[1] if nargs > 1 else kwargs[uid_name]
2480+ return Scope(cr, uid, None)
2481+
2482+ return get
2483+
2484+
2485+def _scope_cr_context_getter(method):
2486+ """ return a function that makes the scope corresponding to parameters """
2487+ names = getargspec(method).args
2488+ cr_name = len(names) > 1 and names[1]
2489+ ctx_pos = names.index('context') - 1 if 'context' in names else 1024
2490+
2491+ def get(args, kwargs):
2492+ nargs = len(args)
2493+ cr = args[0] if nargs > 0 else kwargs[cr_name]
2494+ context = args[ctx_pos] if nargs > ctx_pos else kwargs.get('context')
2495+ return Scope(cr, SUPERUSER_ID, context)
2496+
2497+ return get
2498+
2499+
2500+def _scope_cr_uid_context_getter(method):
2501+ """ return a function that makes the scope corresponding to parameters """
2502+ names = getargspec(method).args
2503+ cr_name = len(names) > 1 and names[1]
2504+ uid_name = len(names) > 2 and names[2]
2505+ ctx_pos = names.index('context') - 1 if 'context' in names else 1024
2506+
2507+ def get(args, kwargs):
2508+ nargs = len(args)
2509+ cr = args[0] if nargs > 0 else kwargs[cr_name]
2510+ uid = args[1] if nargs > 1 else kwargs[uid_name]
2511+ context = args[ctx_pos] if nargs > ctx_pos else kwargs.get('context')
2512+ return Scope(cr, uid, context)
2513+
2514+ return get
2515+
2516+
2517+def model(method):
2518+ """ Decorate a record-style method where `self` is any instance with scope
2519+ (model, record or recordset). Such a method::
2520+
2521+ @api.model
2522+ def method(self, args):
2523+ ...
2524+
2525+ may be called in both record and traditional styles, like::
2526+
2527+ model.method(args)
2528+ model.method(cr, uid, args, context=context)
2529+ """
2530+ method._api = model
2531+ split_args = _cr_uid_context_splitter(method)
2532+ new_to_old = _converter_to_old(method)
2533+
2534+ @wraps(method)
2535+ def model_wrapper(self, *args, **kwargs):
2536+ if _has_cursor(args, kwargs):
2537+ cr, uid, context, args, kwargs = split_args(args, kwargs)
2538+ with Scope(cr, uid, context):
2539+ value = method(self, *args, **kwargs)
2540+ return new_to_old(self, value)
2541+ else:
2542+ return method(self, *args, **kwargs)
2543+
2544+ return model_wrapper
2545+
2546+
2547+def multi(method):
2548+ """ Decorate a record-style method where `self` is a recordset. Such a
2549+ method::
2550+
2551+ @api.multi
2552+ def method(self, args):
2553+ ...
2554+
2555+ may be called in both record and traditional styles, like::
2556+
2557+ recs = model.browse(ids)
2558+
2559+ # the following calls are equivalent
2560+ recs.method(args)
2561+ model.method(cr, uid, ids, args, context=context)
2562+ """
2563+ method._api = multi
2564+ split_args = _cr_uid_ids_context_splitter(method)
2565+ new_to_old = _converter_to_old(method)
2566+
2567+ @wraps(method)
2568+ def multi_wrapper(self, *args, **kwargs):
2569+ if _has_cursor(args, kwargs):
2570+ cr, uid, ids, context, args, kwargs = split_args(args, kwargs)
2571+ with Scope(cr, uid, context):
2572+ value = method(self.browse(ids), *args, **kwargs)
2573+ return new_to_old(self, value)
2574+ else:
2575+ return method(self, *args, **kwargs)
2576+
2577+ return multi_wrapper
2578+
2579+
2580+def one(method):
2581+ """ Decorate a record-style method where `self` is expected to be a
2582+ singleton instance. The decorated method automatically loops on records,
2583+ and makes a list with the results. In case the method is decorated with
2584+ @returns, it concatenates the resulting instances. Such a method::
2585+
2586+ @api.one
2587+ def method(self, args):
2588+ return self.name
2589+
2590+ may be called in both record and traditional styles, like::
2591+
2592+ recs = model.browse(ids)
2593+
2594+ # the following calls are equivalent and return a list of names
2595+ recs.method(args)
2596+ model.method(cr, uid, recs.unbrowse(), args, context=context)
2597+ """
2598+ method._api = one
2599+ split_args = _cr_uid_ids_context_splitter(method)
2600+ new_to_old = _converter_to_old(method)
2601+ aggregate = _aggregator_one(method)
2602+
2603+ @wraps(method)
2604+ def one_wrapper(self, *args, **kwargs):
2605+ if _has_cursor(args, kwargs):
2606+ cr, uid, ids, context, args, kwargs = split_args(args, kwargs)
2607+ with Scope(cr, uid, context):
2608+ value = [method(rec, *args, **kwargs) for rec in self.browse(ids)]
2609+ return new_to_old(self, aggregate(self, value))
2610+ else:
2611+ value = [method(rec, *args, **kwargs) for rec in self]
2612+ return aggregate(self, value)
2613+
2614+ return one_wrapper
2615+
2616+
2617+def cr(method):
2618+ """ Decorate a traditional-style method that takes `cr` as a parameter.
2619+ Such a method may be called in both record and traditional styles, like::
2620+
2621+ obj.method(args) # record style
2622+ obj.method(cr, args) # traditional style
2623+ """
2624+ method._api = cr
2625+ get_scope = _scope_cr_getter(method)
2626+ old_to_new = _converter_to_new(method)
2627+
2628+ @wraps(method)
2629+ def cr_wrapper(self, *args, **kwargs):
2630+ if _has_cursor(args, kwargs):
2631+ with get_scope(args, kwargs):
2632+ return method(self, *args, **kwargs)
2633+ else:
2634+ value = method(self, scope.cr, *args, **kwargs)
2635+ return old_to_new(self, value)
2636+
2637+ return cr_wrapper
2638+
2639+
2640+def cr_context(method):
2641+ """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """
2642+ method._api = cr_context
2643+ get_scope = _scope_cr_context_getter(method)
2644+ old_to_new = _converter_to_new(method)
2645+
2646+ @wraps(method)
2647+ def cr_context_wrapper(self, *args, **kwargs):
2648+ if _has_cursor(args, kwargs):
2649+ with get_scope(args, kwargs):
2650+ return method(self, *args, **kwargs)
2651+ else:
2652+ cr, _, context = scope.args
2653+ kwargs['context'] = context
2654+ value = method(self, cr, *args, **kwargs)
2655+ return old_to_new(self, value)
2656+
2657+ return cr_context_wrapper
2658+
2659+
2660+def cr_uid(method):
2661+ """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
2662+ method._api = cr_uid
2663+ get_scope = _scope_cr_uid_getter(method)
2664+ old_to_new = _converter_to_new(method)
2665+
2666+ @wraps(method)
2667+ def cr_uid_wrapper(self, *args, **kwargs):
2668+ if _has_cursor(args, kwargs):
2669+ with get_scope(args, kwargs):
2670+ return method(self, *args, **kwargs)
2671+ else:
2672+ cr, uid, _ = scope.args
2673+ value = method(self, cr, uid, *args, **kwargs)
2674+ return old_to_new(self, value)
2675+
2676+ return cr_uid_wrapper
2677+
2678+
2679+def cr_uid_context(method):
2680+ """ Decorate a traditional-style method that takes `cr`, `uid`, `context` as
2681+ parameters. Such a method may be called in both record and traditional
2682+ styles, like::
2683+
2684+ obj.method(args)
2685+ obj.method(cr, uid, args, context=context)
2686+ """
2687+ method._api = cr_uid_context
2688+ get_scope = _scope_cr_uid_context_getter(method)
2689+ old_to_new = _converter_to_new(method)
2690+
2691+ @wraps(method)
2692+ def cr_uid_context_wrapper(self, *args, **kwargs):
2693+ if _has_cursor(args, kwargs):
2694+ with get_scope(args, kwargs):
2695+ return method(self, *args, **kwargs)
2696+ else:
2697+ cr, uid, context = scope.args
2698+ kwargs['context'] = context
2699+ value = method(self, cr, uid, *args, **kwargs)
2700+ return old_to_new(self, value)
2701+
2702+ return cr_uid_context_wrapper
2703+
2704+
2705+def cr_uid_id(method):
2706+ """ Decorate a traditional-style method that takes `cr`, `uid`, `id` as
2707+ parameters. Such a method may be called in both record and traditional
2708+ styles. In the record style, the method automatically loops on records.
2709+ """
2710+ method._api = cr_uid_id
2711+ get_scope = _scope_cr_uid_getter(method)
2712+ old_to_new = _converter_to_new(method)
2713+
2714+ @wraps(method)
2715+ def cr_uid_id_wrapper(self, *args, **kwargs):
2716+ if _has_cursor(args, kwargs):
2717+ with get_scope(args, kwargs):
2718+ return method(self, *args, **kwargs)
2719+ else:
2720+ cr, uid, _ = scope.args
2721+ value = [method(self, cr, uid, id, *args, **kwargs) for id in self.unbrowse()]
2722+ return old_to_new(self, value)
2723+
2724+ return cr_uid_id_wrapper
2725+
2726+
2727+def cr_uid_id_context(method):
2728+ """ Decorate a traditional-style method that takes `cr`, `uid`, `id`,
2729+ `context` as parameters. Such a method::
2730+
2731+ @api.cr_uid_id
2732+ def method(self, cr, uid, id, args, context=None):
2733+ ...
2734+
2735+ may be called in both record and traditional styles, like::
2736+
2737+ rec = model.browse(id)
2738+
2739+ # the following calls are equivalent
2740+ rec.method(args)
2741+ model.method(cr, uid, id, args, context=context)
2742+ """
2743+ method._api = cr_uid_id_context
2744+ get_scope = _scope_cr_uid_context_getter(method)
2745+ old_to_new = _converter_to_new(method)
2746+
2747+ @wraps(method)
2748+ def cr_uid_id_context_wrapper(self, *args, **kwargs):
2749+ if _has_cursor(args, kwargs):
2750+ with get_scope(args, kwargs):
2751+ return method(self, *args, **kwargs)
2752+ else:
2753+ cr, uid, context = scope.args
2754+ kwargs['context'] = context
2755+ value = [method(self, cr, uid, id, *args, **kwargs) for id in self.unbrowse()]
2756+ return old_to_new(self, value)
2757+
2758+ return cr_uid_id_context_wrapper
2759+
2760+
2761+def cr_uid_ids(method):
2762+ """ Decorate a traditional-style method that takes `cr`, `uid`, `ids` as
2763+ parameters. Such a method may be called in both record and traditional
2764+ styles.
2765+ """
2766+ method._api = cr_uid_ids
2767+ get_scope = _scope_cr_uid_getter(method)
2768+ old_to_new = _converter_to_new(method)
2769+
2770+ @wraps(method)
2771+ def cr_uid_ids_wrapper(self, *args, **kwargs):
2772+ if _has_cursor(args, kwargs):
2773+ with get_scope(args, kwargs):
2774+ return method(self, *args, **kwargs)
2775+ else:
2776+ cr, uid, _ = scope.args
2777+ value = method(self, cr, uid, self.unbrowse(), *args, **kwargs)
2778+ return old_to_new(self, value)
2779+
2780+ return cr_uid_ids_wrapper
2781+
2782+
2783+def cr_uid_ids_context(method):
2784+ """ Decorate a traditional-style method that takes `cr`, `uid`, `ids`,
2785+ `context` as parameters. Such a method::
2786+
2787+ @api.cr_uid_ids_context
2788+ def method(self, cr, uid, ids, args, context=None):
2789+ ...
2790+
2791+ may be called in both record and traditional styles, like::
2792+
2793+ recs = model.browse(ids)
2794+
2795+ # the following calls are equivalent
2796+ recs.method(args)
2797+ model.method(cr, uid, ids, args, context=context)
2798+
2799+ It is generally not necessary, see :func:`guess`.
2800+ """
2801+ method._api = cr_uid_ids_context
2802+ get_scope = _scope_cr_uid_context_getter(method)
2803+ old_to_new = _converter_to_new(method)
2804+
2805+ @wraps(method)
2806+ def cr_uid_ids_context_wrapper(self, *args, **kwargs):
2807+ if _has_cursor(args, kwargs):
2808+ with get_scope(args, kwargs):
2809+ return method(self, *args, **kwargs)
2810+ else:
2811+ cr, uid, context = scope.args
2812+ kwargs['context'] = context
2813+ value = method(self, cr, uid, self.unbrowse(), *args, **kwargs)
2814+ return old_to_new(self, value)
2815+
2816+ return cr_uid_ids_context_wrapper
2817+
2818+
2819+def _make_wrapper(method, old_api, new_api):
2820+ @wraps(method)
2821+ def wrapper(self, *args, **kwargs):
2822+ if _has_cursor(args, kwargs):
2823+ return old_api(self, *args, **kwargs)
2824+ else:
2825+ return new_api(self, *args, **kwargs)
2826+ return wrapper
2827+
2828+
2829+def old(method):
2830+ """ Decorate `method` so that it accepts the old-style api only. The
2831+ returned wrapper provides a decorator `new` for the corresponding
2832+ new-style api implementation; the result combines both implementations.
2833+ This is useful to provide explicitly both implementations of a method.::
2834+
2835+ @old
2836+ def stuff(self, cr, uid, context=None):
2837+ ...
2838+
2839+ @stuff.new
2840+ def stuff(self):
2841+ ...
2842+ """
2843+ wrapper = _make_wrapper(method, method, None)
2844+ wrapper.new = lambda new_api: _make_wrapper(method, method, new_api)
2845+ return wrapper
2846+
2847+
2848+def new(method):
2849+ """ Decorate `method` so that it accepts the new-style api only. The
2850+ returned wrapper provides a decorator `old` for the corresponding
2851+ old-style api implementation; the result combines both implementations.
2852+ This is useful to provide explicitly both implementations of a method.::
2853+
2854+ @new
2855+ def stuff(self):
2856+ ...
2857+
2858+ @stuff.old
2859+ def stuff(self, cr, uid, context=None):
2860+ ...
2861+ """
2862+ wrapper = _make_wrapper(method, None, method)
2863+ wrapper.old = lambda old_api: _make_wrapper(method, old_api, method)
2864+ return wrapper
2865+
2866+
2867+def noguess(method):
2868+ """ Decorate a method to prevent any effect from :func:`guess`. """
2869+ method._api = False
2870+ return method
2871+
2872+
2873+def guess(method):
2874+ """ Decorate `method` to make it callable in both traditional and record
2875+ styles. This decorator is applied automatically by the model's
2876+ metaclass, and has no effect on already-decorated methods.
2877+
2878+ The API style is determined by heuristics on the parameter names: ``cr``
2879+ or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
2880+ ``id`` or ``ids`` for a list of record ids, and ``context`` for the
2881+ context dictionary. If a traditional API is recognized, one of the
2882+ decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
2883+ :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
2884+ :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
2885+
2886+ Method calls are considered traditional style when their first parameter
2887+ is a database cursor.
2888+ """
2889+ # introspection on argument names to determine api style
2890+ names = tuple(getargspec(method).args) + (None,) * 4
2891+
2892+ if names[0] == 'self':
2893+ if names[1] in ('cr', 'cursor'):
2894+ if names[2] in ('uid', 'user'):
2895+ if names[3] == 'ids':
2896+ if 'context' in names:
2897+ return cr_uid_ids_context(method)
2898+ else:
2899+ return cr_uid_ids(method)
2900+ elif names[3] == 'id':
2901+ if 'context' in names:
2902+ return cr_uid_id_context(method)
2903+ else:
2904+ return cr_uid_id(method)
2905+ elif 'context' in names:
2906+ return cr_uid_context(method)
2907+ else:
2908+ return cr_uid(method)
2909+ elif 'context' in names:
2910+ return cr_context(method)
2911+ else:
2912+ return cr(method)
2913+
2914+ # no wrapping by default
2915+ return noguess(method)
2916+
2917+
2918+def expected(decorator, func):
2919+ """ Decorate `func` with `decorator` if `func` is not wrapped yet. """
2920+ return decorator(func) if not hasattr(func, '_orig') else func
2921+
2922+
2923+# keep those imports here in order to handle cyclic dependencies correctly
2924+from openerp import SUPERUSER_ID
2925+from openerp.osv.scope import Scope, proxy as scope
2926+from openerp.sql_db import Cursor
2927
2928=== modified file 'openerp/osv/expression.py'
2929--- openerp/osv/expression.py 2014-01-15 20:53:57 +0000
2930+++ openerp/osv/expression.py 2014-01-22 16:19:54 +0000
2931@@ -137,7 +137,7 @@
2932
2933 import openerp.modules
2934 from openerp.osv import fields
2935-from openerp.osv.orm import MAGIC_COLUMNS
2936+from openerp.osv.orm import MAGIC_COLUMNS, BaseModel
2937 import openerp.tools as tools
2938
2939
2940@@ -512,7 +512,7 @@
2941 in the condition (i.e. in many2one); this link is used to
2942 compute aliases
2943 """
2944- assert model, 'Invalid leaf creation without table'
2945+ assert isinstance(model, BaseModel), 'Invalid leaf creation without table'
2946 self.join_context = join_context or []
2947 self.leaf = leaf
2948 # normalize the leaf's operator
2949@@ -673,20 +673,23 @@
2950 - the leaf is added to the result
2951
2952 Some internal var explanation:
2953- :var obj working_model: model object, model containing the field
2954+ :var list path: left operand seen as a sequence of field names
2955+ ("foo.bar" -> ["foo", "bar"])
2956+ :var obj model: model object, model containing the field
2957 (the name provided in the left operand)
2958- :var list field_path: left operand seen as a path (foo.bar -> [foo, bar])
2959- :var obj relational_model: relational model of a field (field._obj)
2960- ex: res_partner.bank_ids -> res.partner.bank
2961+ :var obj field: the field corresponding to `path[0]`
2962+ :var obj column: the column corresponding to `path[0]`
2963+ :var obj comodel: relational model of field (field.comodel)
2964+ (res_partner.bank_ids -> res.partner.bank)
2965 """
2966
2967- def to_ids(value, relational_model, context=None, limit=None):
2968+ def to_ids(value, comodel, context=None, limit=None):
2969 """ Normalize a single id or name, or a list of those, into a list of ids
2970 :param {int,long,basestring,list,tuple} value:
2971 if int, long -> return [value]
2972 if basestring, convert it into a list of basestrings, then
2973 if list of basestring ->
2974- perform a name_search on relational_model for each name
2975+ perform a name_search on comodel for each name
2976 return the list of related ids
2977 """
2978 names = []
2979@@ -697,7 +700,7 @@
2980 elif isinstance(value, (int, long)):
2981 return [value]
2982 if names:
2983- name_get_list = [name_get[0] for name in names for name_get in relational_model.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)]
2984+ name_get_list = [name_get[0] for name in names for name_get in comodel.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)]
2985 return list(set(name_get_list))
2986 return list(value)
2987
2988@@ -747,7 +750,6 @@
2989 leaf = pop()
2990
2991 # Get working variables
2992- working_model = leaf.model
2993 if leaf.is_operator():
2994 left, operator, right = leaf.leaf, None, None
2995 elif leaf.is_true_leaf() or leaf.is_false_leaf():
2996@@ -755,12 +757,12 @@
2997 left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2])
2998 else:
2999 left, operator, right = leaf.leaf
3000- field_path = left.split('.', 1)
3001- field = working_model._columns.get(field_path[0])
3002- if field and field._obj:
3003- relational_model = working_model.pool[field._obj]
3004- else:
3005- relational_model = None
3006+ path = left.split('.', 1)
3007+
3008+ model = leaf.model
3009+ field = model._fields.get(path[0])
3010+ column = model._columns.get(path[0])
3011+ comodel = getattr(field, 'comodel', None)
3012
3013 # ----------------------------------------
3014 # SIMPLE CASE
3015@@ -783,22 +785,22 @@
3016 # -> else: crash
3017 # ----------------------------------------
3018
3019- elif not field and field_path[0] in working_model._inherit_fields:
3020+ elif not column and path[0] in model._inherit_fields:
3021 # comments about inherits'd fields
3022 # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',
3023 # field_column_obj, origina_parent_model), ... }
3024- next_model = working_model.pool[working_model._inherit_fields[field_path[0]][0]]
3025- leaf.add_join_context(next_model, working_model._inherits[next_model._name], 'id', working_model._inherits[next_model._name])
3026+ next_model = model.pool[model._inherit_fields[path[0]][0]]
3027+ leaf.add_join_context(next_model, model._inherits[next_model._name], 'id', model._inherits[next_model._name])
3028 push(leaf)
3029
3030 elif left == 'id' and operator == 'child_of':
3031- ids2 = to_ids(right, working_model, context)
3032- dom = child_of_domain(left, ids2, working_model)
3033+ ids2 = to_ids(right, model, context)
3034+ dom = child_of_domain(left, ids2, model)
3035 for dom_leaf in reversed(dom):
3036- new_leaf = create_substitution_leaf(leaf, dom_leaf, working_model)
3037+ new_leaf = create_substitution_leaf(leaf, dom_leaf, model)
3038 push(new_leaf)
3039
3040- elif not field and field_path[0] in MAGIC_COLUMNS:
3041+ elif not column and path[0] in MAGIC_COLUMNS:
3042 push_result(leaf)
3043
3044 elif not field:
3045@@ -807,70 +809,88 @@
3046 # ----------------------------------------
3047 # PATH SPOTTED
3048 # -> many2one or one2many with _auto_join:
3049- # - add a join, then jump into linked field: field.remaining on
3050+ # - add a join, then jump into linked column: column.remaining on
3051 # src_table is replaced by remaining on dst_table, and set for re-evaluation
3052- # - if a domain is defined on the field, add it into evaluation
3053+ # - if a domain is defined on the column, add it into evaluation
3054 # on the relational table
3055 # -> many2one, many2many, one2many: replace by an equivalent computed
3056 # domain, given by recursively searching on the remaining of the path
3057- # -> note: hack about fields.property should not be necessary anymore
3058- # as after transforming the field, it will go through this loop once again
3059+ # -> note: hack about columns.property should not be necessary anymore
3060+ # as after transforming the column, it will go through this loop once again
3061 # ----------------------------------------
3062
3063- elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join:
3064+ elif len(path) > 1 and column._type == 'many2one' and column._auto_join:
3065 # res_partner.state_id = res_partner__state_id.id
3066- leaf.add_join_context(relational_model, field_path[0], 'id', field_path[0])
3067- push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))
3068+ leaf.add_join_context(comodel, path[0], 'id', path[0])
3069+ push(create_substitution_leaf(leaf, (path[1], operator, right), comodel))
3070
3071- elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join:
3072+ elif len(path) > 1 and column._type == 'one2many' and column._auto_join:
3073 # res_partner.id = res_partner__bank_ids.partner_id
3074- leaf.add_join_context(relational_model, 'id', field._fields_id, field_path[0])
3075- domain = field._domain(working_model) if callable(field._domain) else field._domain
3076- push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))
3077+ leaf.add_join_context(comodel, 'id', column._fields_id, path[0])
3078+ domain = column._domain(model) if callable(column._domain) else column._domain
3079+ push(create_substitution_leaf(leaf, (path[1], operator, right), comodel))
3080 if domain:
3081 domain = normalize_domain(domain)
3082 for elem in reversed(domain):
3083- push(create_substitution_leaf(leaf, elem, relational_model))
3084- push(create_substitution_leaf(leaf, AND_OPERATOR, relational_model))
3085-
3086- elif len(field_path) > 1 and field._auto_join:
3087- raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % left)
3088-
3089- elif len(field_path) > 1 and field._type == 'many2one':
3090- right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)
3091- leaf.leaf = (field_path[0], 'in', right_ids)
3092+ push(create_substitution_leaf(leaf, elem, comodel))
3093+ push(create_substitution_leaf(leaf, AND_OPERATOR, comodel))
3094+
3095+ elif len(path) > 1 and column._auto_join:
3096+ raise NotImplementedError('_auto_join attribute not supported on many2many column %s' % left)
3097+
3098+ elif len(path) > 1 and column._type == 'many2one':
3099+ right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=context)
3100+ leaf.leaf = (path[0], 'in', right_ids)
3101 push(leaf)
3102
3103- # Making search easier when there is a left operand as field.o2m or field.m2m
3104- elif len(field_path) > 1 and field._type in ['many2many', 'one2many']:
3105- right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)
3106- table_ids = working_model.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False))
3107+ # Making search easier when there is a left operand as column.o2m or column.m2m
3108+ elif len(path) > 1 and column._type in ['many2many', 'one2many']:
3109+ right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=context)
3110+ table_ids = model.search(cr, uid, [(path[0], 'in', right_ids)], context=dict(context, active_test=False))
3111 leaf.leaf = ('id', 'in', table_ids)
3112 push(leaf)
3113
3114+ elif not field.store:
3115+ # Non-stored field should provide an implementation of search.
3116+ if not field.search:
3117+ # field does not support search!
3118+ _logger.error("Non-stored field %s cannot be searched.", field)
3119+ if _logger.isEnabledFor(logging.DEBUG):
3120+ _logger.debug(''.join(traceback.format_stack()))
3121+ # Ignore it: generate a dummy leaf.
3122+ domain = []
3123+ else:
3124+ # Let the field generate a domain.
3125+ domain = field.determine_domain(operator, right)
3126+
3127+ if not domain:
3128+ leaf.leaf = TRUE_LEAF
3129+ push(leaf)
3130+ else:
3131+ for elem in reversed(domain):
3132+ push(create_substitution_leaf(leaf, elem, model))
3133+
3134 # -------------------------------------------------
3135 # FUNCTION FIELD
3136 # -> not stored: error if no _fnct_search, otherwise handle the result domain
3137 # -> stored: management done in the remaining of parsing
3138 # -------------------------------------------------
3139
3140- elif isinstance(field, fields.function) and not field.store and not field._fnct_search:
3141+ elif isinstance(column, fields.function) and not column.store:
3142 # this is a function field that is not stored
3143- # the function field doesn't provide a search function and doesn't store
3144- # values in the database, so we must ignore it : we generate a dummy leaf
3145- leaf.leaf = TRUE_LEAF
3146- _logger.error(
3147- "The field '%s' (%s) can not be searched: non-stored "
3148- "function field without fnct_search",
3149- field.string, left)
3150- # avoid compiling stack trace if not needed
3151- if _logger.isEnabledFor(logging.DEBUG):
3152- _logger.debug(''.join(traceback.format_stack()))
3153- push(leaf)
3154+ if not column._fnct_search:
3155+ _logger.error(
3156+ "Field '%s' (%s) can not be searched: "
3157+ "non-stored function field without fnct_search",
3158+ column.string, left)
3159+ # avoid compiling stack trace if not needed
3160+ if _logger.isEnabledFor(logging.DEBUG):
3161+ _logger.debug(''.join(traceback.format_stack()))
3162+ # ignore it: generate a dummy leaf
3163+ fct_domain = []
3164+ else:
3165+ fct_domain = column.search(cr, uid, model, left, [leaf.leaf], context=context)
3166
3167- elif isinstance(field, fields.function) and not field.store:
3168- # this is a function field that is not stored
3169- fct_domain = field.search(cr, uid, working_model, left, [leaf.leaf], context=context)
3170 if not fct_domain:
3171 leaf.leaf = TRUE_LEAF
3172 push(leaf)
3173@@ -878,30 +898,30 @@
3174 # we assume that the expression is valid
3175 # we create a dummy leaf for forcing the parsing of the resulting expression
3176 for domain_element in reversed(fct_domain):
3177- push(create_substitution_leaf(leaf, domain_element, working_model))
3178- # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_model))
3179- # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_model))
3180+ push(create_substitution_leaf(leaf, domain_element, model))
3181+ # self.push(create_substitution_leaf(leaf, TRUE_LEAF, model))
3182+ # self.push(create_substitution_leaf(leaf, AND_OPERATOR, model))
3183
3184 # -------------------------------------------------
3185 # RELATIONAL FIELDS
3186 # -------------------------------------------------
3187
3188 # Applying recursivity on field(one2many)
3189- elif field._type == 'one2many' and operator == 'child_of':
3190- ids2 = to_ids(right, relational_model, context)
3191- if field._obj != working_model._name:
3192- dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)
3193+ elif column._type == 'one2many' and operator == 'child_of':
3194+ ids2 = to_ids(right, comodel, context)
3195+ if column._obj != model._name:
3196+ dom = child_of_domain(left, ids2, comodel, prefix=column._obj)
3197 else:
3198- dom = child_of_domain('id', ids2, working_model, parent=left)
3199+ dom = child_of_domain('id', ids2, model, parent=left)
3200 for dom_leaf in reversed(dom):
3201- push(create_substitution_leaf(leaf, dom_leaf, working_model))
3202+ push(create_substitution_leaf(leaf, dom_leaf, model))
3203
3204- elif field._type == 'one2many':
3205+ elif column._type == 'one2many':
3206 call_null = True
3207
3208 if right is not False:
3209 if isinstance(right, basestring):
3210- ids2 = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context, limit=None)]
3211+ ids2 = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context, limit=None)]
3212 if ids2:
3213 operator = 'in'
3214 else:
3215@@ -913,36 +933,36 @@
3216 if operator in ['like', 'ilike', 'in', '=']:
3217 #no result found with given search criteria
3218 call_null = False
3219- push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))
3220+ push(create_substitution_leaf(leaf, FALSE_LEAF, model))
3221 else:
3222- ids2 = select_from_where(cr, field._fields_id, relational_model._table, 'id', ids2, operator)
3223+ ids2 = select_from_where(cr, column._fields_id, comodel._table, 'id', ids2, operator)
3224 if ids2:
3225 call_null = False
3226 o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
3227- push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_model))
3228+ push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), model))
3229
3230 if call_null:
3231 o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
3232- push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_model._table)), working_model))
3233+ push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model))
3234
3235- elif field._type == 'many2many':
3236- rel_table, rel_id1, rel_id2 = field._sql_names(working_model)
3237+ elif column._type == 'many2many':
3238+ rel_table, rel_id1, rel_id2 = column._sql_names(model)
3239 #FIXME
3240 if operator == 'child_of':
3241 def _rec_convert(ids):
3242- if relational_model == working_model:
3243+ if comodel == model:
3244 return ids
3245 return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)
3246
3247- ids2 = to_ids(right, relational_model, context)
3248- dom = child_of_domain('id', ids2, relational_model)
3249- ids2 = relational_model.search(cr, uid, dom, context=context)
3250- push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_model))
3251+ ids2 = to_ids(right, comodel, context)
3252+ dom = child_of_domain('id', ids2, comodel)
3253+ ids2 = comodel.search(cr, uid, dom, context=context)
3254+ push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model))
3255 else:
3256 call_null_m2m = True
3257 if right is not False:
3258 if isinstance(right, basestring):
3259- res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context)]
3260+ res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context)]
3261 if res_ids:
3262 operator = 'in'
3263 else:
3264@@ -954,29 +974,29 @@
3265 if operator in ['like', 'ilike', 'in', '=']:
3266 #no result found with given search criteria
3267 call_null_m2m = False
3268- push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))
3269+ push(create_substitution_leaf(leaf, FALSE_LEAF, model))
3270 else:
3271 operator = 'in' # operator changed because ids are directly related to main object
3272 else:
3273 call_null_m2m = False
3274 m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
3275- push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_model))
3276+ push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model))
3277
3278 if call_null_m2m:
3279 m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
3280- push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_model))
3281+ push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), model))
3282
3283- elif field._type == 'many2one':
3284+ elif column._type == 'many2one':
3285 if operator == 'child_of':
3286- ids2 = to_ids(right, relational_model, context)
3287- if field._obj != working_model._name:
3288- dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)
3289+ ids2 = to_ids(right, comodel, context)
3290+ if column._obj != model._name:
3291+ dom = child_of_domain(left, ids2, comodel, prefix=column._obj)
3292 else:
3293- dom = child_of_domain('id', ids2, working_model, parent=left)
3294+ dom = child_of_domain('id', ids2, model, parent=left)
3295 for dom_leaf in reversed(dom):
3296- push(create_substitution_leaf(leaf, dom_leaf, working_model))
3297+ push(create_substitution_leaf(leaf, dom_leaf, model))
3298 else:
3299- def _get_expression(relational_model, cr, uid, left, right, operator, context=None):
3300+ def _get_expression(comodel, cr, uid, left, right, operator, context=None):
3301 if context is None:
3302 context = {}
3303 c = context.copy()
3304@@ -991,14 +1011,14 @@
3305 operator = dict_op[operator]
3306 elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2'])
3307 operator = dict_op[operator]
3308- res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, limit=None, context=c)]
3309+ res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, limit=None, context=c)]
3310 if operator in NEGATIVE_TERM_OPERATORS:
3311 res_ids.append(False) # TODO this should not be appended if False was in 'right'
3312 return left, 'in', res_ids
3313 # resolve string-based m2o criterion into IDs
3314 if isinstance(right, basestring) or \
3315 right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right):
3316- push(create_substitution_leaf(leaf, _get_expression(relational_model, cr, uid, left, right, operator, context=context), working_model))
3317+ push(create_substitution_leaf(leaf, _get_expression(comodel, cr, uid, left, right, operator, context=context), model))
3318 else:
3319 # right == [] or right == False and all other cases are handled by __leaf_to_sql()
3320 push_result(leaf)
3321@@ -1006,19 +1026,19 @@
3322 # -------------------------------------------------
3323 # OTHER FIELDS
3324 # -> datetime fields: manage time part of the datetime
3325- # field when it is not there
3326+ # column when it is not there
3327 # -> manage translatable fields
3328 # -------------------------------------------------
3329
3330 else:
3331- if field._type == 'datetime' and right and len(right) == 10:
3332+ if column._type == 'datetime' and right and len(right) == 10:
3333 if operator in ('>', '>=', '='):
3334 right += ' 00:00:00'
3335 elif operator in ('<', '<='):
3336 right += ' 23:59:59'
3337- push(create_substitution_leaf(leaf, (left, operator, right), working_model))
3338+ push(create_substitution_leaf(leaf, (left, operator, right), model))
3339
3340- elif field.translate:
3341+ elif column.translate:
3342 need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
3343 sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
3344 if need_wildcard:
3345@@ -1042,22 +1062,22 @@
3346 subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \
3347 ') UNION (' \
3348 ' SELECT id' \
3349- ' FROM "' + working_model._table + '"' \
3350+ ' FROM "' + model._table + '"' \
3351 ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))"
3352 else:
3353 subselect += ' AND value ' + sql_operator + instr + \
3354 ') UNION (' \
3355 ' SELECT id' \
3356- ' FROM "' + working_model._table + '"' \
3357+ ' FROM "' + model._table + '"' \
3358 ' WHERE "' + left + '" ' + sql_operator + instr + ")"
3359
3360- params = [working_model._name + ',' + left,
3361+ params = [model._name + ',' + left,
3362 context.get('lang', False) or 'en_US',
3363 'model',
3364 right,
3365 right,
3366 ]
3367- push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), working_model))
3368+ push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), model))
3369
3370 else:
3371 push_result(leaf)
3372
3373=== modified file 'openerp/osv/fields.py'
3374--- openerp/osv/fields.py 2014-01-15 20:53:57 +0000
3375+++ openerp/osv/fields.py 2014-01-22 16:19:54 +0000
3376@@ -93,6 +93,9 @@
3377 """
3378 if domain is None:
3379 domain = []
3380+ elif callable(domain):
3381+ from openerp import api
3382+ domain = api.expected(api.cr_uid_context, domain)
3383 if context is None:
3384 context = {}
3385 self.states = states or {}
3386@@ -117,7 +120,35 @@
3387 self.deprecated = False # Optional deprecation warning
3388 for a in args:
3389 setattr(self, a, args[a])
3390-
3391+
3392+ # prefetch only if self._classic_write, not self.groups, and not
3393+ # self.deprecated
3394+ if not self._classic_write or self.groups or self.deprecated:
3395+ self._prefetch = False
3396+
3397+ def to_field(self):
3398+ """ convert column `self` to a new-style field """
3399+ from openerp.osv.fields2 import Field
3400+ return Field.by_type[self._type](**self.to_field_args())
3401+
3402+ def to_field_args(self):
3403+ """ return a dictionary with all the arguments to pass to the field """
3404+ items = [
3405+ ('interface_for', self), # field interfaces self
3406+ ('string', self.string),
3407+ ('help', self.help),
3408+ ('readonly', self.readonly),
3409+ ('required', self.required),
3410+ ('states', self.states),
3411+ ('groups', self.groups),
3412+ ('size', self.size),
3413+ ('ondelete', self.ondelete),
3414+ ('translate', self.translate),
3415+ ('domain', self._domain),
3416+ ('context', self._context),
3417+ ]
3418+ return dict(item for item in items if items[1])
3419+
3420 def restart(self):
3421 pass
3422
3423@@ -182,8 +213,16 @@
3424 _classic_read = False # post-process to handle missing target
3425
3426 def __init__(self, string, selection, size=None, **args):
3427+ if callable(selection):
3428+ from openerp import api
3429+ selection = api.expected(api.cr_uid_context, selection)
3430 _column.__init__(self, string=string, size=size, selection=selection, **args)
3431
3432+ def to_field_args(self):
3433+ args = super(reference, self).to_field_args()
3434+ args['selection'] = self.selection
3435+ return args
3436+
3437 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
3438 result = {}
3439 # copy initial values fetched previously.
3440@@ -231,7 +270,6 @@
3441 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
3442 self._symbol_set = (self._symbol_c, self._symbol_f)
3443
3444-
3445 class text(_column):
3446 _type = 'text'
3447
3448@@ -260,6 +298,11 @@
3449 # synopsis: digits_compute(cr) -> (precision, scale)
3450 self.digits_compute = digits_compute
3451
3452+ def to_field_args(self):
3453+ args = super(float, self).to_field_args()
3454+ args['digits'] = self.digits_compute or self.digits
3455+ return args
3456+
3457 def digits_change(self, cr):
3458 if self.digits_compute:
3459 self.digits = self.digits_compute(cr)
3460@@ -321,7 +364,8 @@
3461 if context and context.get('tz'):
3462 tz_name = context['tz']
3463 else:
3464- tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
3465+ user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
3466+ tz_name = user.tz
3467 if tz_name:
3468 try:
3469 utc = pytz.timezone('UTC')
3470@@ -385,7 +429,8 @@
3471 tz_name = context['tz']
3472 else:
3473 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
3474- tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
3475+ user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
3476+ tz_name = user.tz
3477 if tz_name:
3478 try:
3479 utc = pytz.timezone('UTC')
3480@@ -448,9 +493,17 @@
3481 _type = 'selection'
3482
3483 def __init__(self, selection, string='unknown', **args):
3484+ if callable(selection):
3485+ from openerp import api
3486+ selection = api.expected(api.cr_uid_context, selection)
3487 _column.__init__(self, string=string, **args)
3488 self.selection = selection
3489
3490+ def to_field_args(self):
3491+ args = super(selection, self).to_field_args()
3492+ args['selection'] = self.selection
3493+ return args
3494+
3495 # ---------------------------------------------------------
3496 # Relationals fields
3497 # ---------------------------------------------------------
3498@@ -477,6 +530,12 @@
3499 self._obj = obj
3500 self._auto_join = auto_join
3501
3502+ def to_field_args(self):
3503+ args = super(many2one, self).to_field_args()
3504+ args['comodel_name'] = self._obj
3505+ args['auto_join'] = self._auto_join
3506+ return args
3507+
3508 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
3509 if context is None:
3510 context = {}
3511@@ -530,7 +589,6 @@
3512 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
3513 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
3514
3515-
3516 @classmethod
3517 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
3518 return value[1] if isinstance(value, tuple) else tools.ustr(value)
3519@@ -551,26 +609,34 @@
3520 #one2many can't be used as condition for defaults
3521 assert(self.change_default != True)
3522
3523+ def to_field_args(self):
3524+ args = super(one2many, self).to_field_args()
3525+ args['comodel_name'] = self._obj
3526+ args['inverse_name'] = self._fields_id
3527+ args['auto_join'] = self._auto_join
3528+ args['limit'] = self._limit
3529+ return args
3530+
3531 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
3532- if context is None:
3533- context = {}
3534 if self._context:
3535- context = context.copy()
3536- context.update(self._context)
3537- if values is None:
3538- values = {}
3539-
3540- res = {}
3541- for id in ids:
3542- res[id] = []
3543-
3544- domain = self._domain(obj) if callable(self._domain) else self._domain
3545- model = obj.pool[self._obj]
3546- ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
3547- for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
3548- if r[self._fields_id] in res:
3549- res[r[self._fields_id]].append(r['id'])
3550- return res
3551+ context = dict(context or {})
3552+ context.update(self._context)
3553+
3554+ with Scope(cr, user, context):
3555+ res = dict((id, []) for id in ids)
3556+
3557+ comodel = obj.pool[self._obj]
3558+ inverse = self._fields_id
3559+ domain = self._domain
3560+ if callable(domain):
3561+ domain = domain(obj)
3562+ domain = domain + [(inverse, 'in', ids)]
3563+
3564+ for record in comodel.search(domain, limit=self._limit):
3565+ assert int(record[inverse]) in res
3566+ res[int(record[inverse])].append(record.id)
3567+
3568+ return res
3569
3570 def set(self, cr, obj, id, field, values, user=None, context=None):
3571 result = []
3572@@ -632,7 +698,6 @@
3573 domain = self._domain(obj) if callable(self._domain) else self._domain
3574 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
3575
3576-
3577 @classmethod
3578 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
3579 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
3580@@ -691,6 +756,15 @@
3581 self._id2 = id2
3582 self._limit = limit
3583
3584+ def to_field_args(self):
3585+ args = super(many2many, self).to_field_args()
3586+ args['comodel_name'] = self._obj
3587+ args['relation'] = self._rel
3588+ args['column1'] = self._id1
3589+ args['column2'] = self._id2
3590+ args['limit'] = self._limit
3591+ return args
3592+
3593 def _sql_names(self, source_model):
3594 """Return the SQL names defining the structure of the m2m relationship table
3595
3596@@ -1084,6 +1158,9 @@
3597
3598 self.digits = args.get('digits', (16,2))
3599 self.digits_compute = args.get('digits_compute', None)
3600+ if callable(args.get('selection')):
3601+ from openerp import api
3602+ self.selection = api.expected(api.cr_uid_context, args['selection'])
3603
3604 self._fnct_inv_arg = fnct_inv_arg
3605 if not fnct_inv:
3606@@ -1105,25 +1182,26 @@
3607 else:
3608 self._prefetch = True
3609
3610- if type == 'float':
3611- self._symbol_c = float._symbol_c
3612- self._symbol_f = float._symbol_f
3613- self._symbol_set = float._symbol_set
3614-
3615- if type == 'boolean':
3616- self._symbol_c = boolean._symbol_c
3617- self._symbol_f = boolean._symbol_f
3618- self._symbol_set = boolean._symbol_set
3619-
3620- if type == 'integer':
3621- self._symbol_c = integer._symbol_c
3622- self._symbol_f = integer._symbol_f
3623- self._symbol_set = integer._symbol_set
3624-
3625 if type == 'char':
3626 self._symbol_c = char._symbol_c
3627 self._symbol_f = lambda x: _symbol_set_char(self, x)
3628 self._symbol_set = (self._symbol_c, self._symbol_f)
3629+ else:
3630+ type_class = globals().get(type)
3631+ if type_class is not None:
3632+ self._symbol_c = type_class._symbol_c
3633+ self._symbol_f = type_class._symbol_f
3634+ self._symbol_set = type_class._symbol_set
3635+
3636+ def to_field_args(self):
3637+ args = super(function, self).to_field_args()
3638+ if self._type in ('float',):
3639+ args['digits'] = self.digits_compute or self.digits
3640+ elif self._type in ('selection', 'reference'):
3641+ args['selection'] = self.selection
3642+ elif self._type in ('many2one', 'one2many', 'many2many'):
3643+ args['comodel_name'] = self._obj
3644+ return args
3645
3646 def digits_change(self, cr):
3647 if self._type == 'float':
3648@@ -1139,7 +1217,8 @@
3649 if not self._fnct_search:
3650 #CHECKME: should raise an exception
3651 return []
3652- return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
3653+ with Scope(cr, uid, context):
3654+ return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
3655
3656 def postprocess(self, cr, uid, obj, field, value=None, context=None):
3657 if context is None:
3658@@ -1170,7 +1249,8 @@
3659 return result
3660
3661 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
3662- result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
3663+ with Scope(cr, uid, context):
3664+ result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
3665 for id in ids:
3666 if self._multi and id in result:
3667 for field, value in result[id].iteritems():
3668@@ -1184,7 +1264,8 @@
3669 if not context:
3670 context = {}
3671 if self._fnct_inv:
3672- self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
3673+ with Scope(cr, user, context):
3674+ self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
3675
3676 @classmethod
3677 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
3678@@ -1213,45 +1294,36 @@
3679 field = '.'.join(self._arg)
3680 return map(lambda x: (field, x[1], x[2]), domain)
3681
3682- def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
3683+ def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
3684 if isinstance(ids, (int, long)):
3685 ids = [ids]
3686- for record in obj.browse(cr, uid, ids, context=context):
3687+ for instance in obj.browse(cr, uid, ids, context=context):
3688 # traverse all fields except the last one
3689 for field in self.arg[:-1]:
3690- record = record[field] or False
3691- if not record:
3692- break
3693- elif isinstance(record, list):
3694- # record is the result of a one2many or many2many field
3695- record = record[0]
3696- if record:
3697- # write on the last field
3698- record.write({self.arg[-1]: values})
3699+ instance = instance[field]
3700+ if instance:
3701+ # write on the last field of the first record
3702+ instance[0].write({self.arg[-1]: values})
3703
3704 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
3705 res = {}
3706 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
3707 value = record
3708 for field in self.arg:
3709- if isinstance(value, list):
3710- value = value[0]
3711- value = value[field] or False
3712- if not value:
3713- break
3714+ value = value[field]
3715 res[record.id] = value
3716
3717 if self._type == 'many2one':
3718- # res[id] is a browse_record or False; convert it to (id, name) or False.
3719+ # res[id] is a recordset; convert it to (id, name) or False.
3720 # Perform name_get as root, as seeing the name of a related object depends on
3721 # access right of source document, not target, so user may not have access.
3722 value_ids = list(set(value.id for value in res.itervalues() if value))
3723 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
3724- res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())
3725+ res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
3726
3727 elif self._type in ('one2many', 'many2many'):
3728- # res[id] is a list of browse_record or False; convert it to a list of ids
3729- res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())
3730+ # res[id] is a recordset; convert it to a list of ids
3731+ res = dict((id, value.unbrowse()) for id, value in res.iteritems())
3732
3733 return res
3734
3735@@ -1445,7 +1517,7 @@
3736 default_val = self._get_default(obj, cr, uid, prop_name, context)
3737
3738 property_create = False
3739- if isinstance(default_val, openerp.osv.orm.browse_record):
3740+ if isinstance(default_val, openerp.osv.orm.BaseModel):
3741 if default_val.id != id_val:
3742 property_create = True
3743 elif default_val != id_val:
3744@@ -1532,52 +1604,6 @@
3745 self.field_id = {}
3746
3747
3748-def field_to_dict(model, cr, user, field, context=None):
3749- """ Return a dictionary representation of a field.
3750-
3751- The string, help, and selection attributes (if any) are untranslated. This
3752- representation is the one returned by fields_get() (fields_get() will do
3753- the translation).
3754-
3755- """
3756-
3757- res = {'type': field._type}
3758- # some attributes for m2m/function field are added as debug info only
3759- if isinstance(field, function):
3760- res['function'] = field._fnct and field._fnct.func_name or False
3761- res['store'] = field.store
3762- if isinstance(field.store, dict):
3763- res['store'] = str(field.store)
3764- res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
3765- res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
3766- res['fnct_inv_arg'] = field._fnct_inv_arg or False
3767- if isinstance(field, many2many):
3768- (table, col1, col2) = field._sql_names(model)
3769- res['m2m_join_columns'] = [col1, col2]
3770- res['m2m_join_table'] = table
3771- for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
3772- 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
3773- 'deprecated', 'digits', 'invisible', 'filters'):
3774- if getattr(field, arg, None):
3775- res[arg] = getattr(field, arg)
3776-
3777- if hasattr(field, 'selection'):
3778- if isinstance(field.selection, (tuple, list)):
3779- res['selection'] = field.selection
3780- else:
3781- # call the 'dynamic selection' function
3782- res['selection'] = field.selection(model, cr, user, context)
3783- if res['type'] in ('one2many', 'many2many', 'many2one'):
3784- res['relation'] = field._obj
3785- res['domain'] = field._domain(model) if callable(field._domain) else field._domain
3786- res['context'] = field._context
3787-
3788- if isinstance(field, one2many):
3789- res['relation_field'] = field._fields_id
3790-
3791- return res
3792-
3793-
3794 class column_info(object):
3795 """ Struct containing details about an osv column, either one local to
3796 its model, or one inherited via _inherits.
3797@@ -1618,5 +1644,7 @@
3798 self.__class__.__name__, self.name, self.column,
3799 self.parent_model, self.parent_column, self.original_parent)
3800
3801+
3802+from openerp.osv.scope import Scope
3803+
3804 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
3805-
3806
3807=== added file 'openerp/osv/fields2.py'
3808--- openerp/osv/fields2.py 1970-01-01 00:00:00 +0000
3809+++ openerp/osv/fields2.py 2014-01-22 16:19:54 +0000
3810@@ -0,0 +1,995 @@
3811+# -*- coding: utf-8 -*-
3812+##############################################################################
3813+#
3814+# OpenERP, Open Source Management Solution
3815+# Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
3816+#
3817+# This program is free software: you can redistribute it and/or modify
3818+# it under the terms of the GNU Affero General Public License as
3819+# published by the Free Software Foundation, either version 3 of the
3820+# License, or (at your option) any later version.
3821+#
3822+# This program is distributed in the hope that it will be useful,
3823+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3824+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3825+# GNU Affero General Public License for more details.
3826+#
3827+# You should have received a copy of the GNU Affero General Public License
3828+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3829+#
3830+##############################################################################
3831+
3832+""" High-level objects for fields. """
3833+
3834+from copy import copy
3835+from datetime import date, datetime
3836+from functools import partial
3837+from operator import attrgetter
3838+import logging
3839+
3840+from openerp.tools import float_round, ustr, html_sanitize, lazy_property
3841+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
3842+from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
3843+
3844+DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
3845+DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
3846+
3847+_logger = logging.getLogger(__name__)
3848+
3849+
3850+class SpecialValue(object):
3851+ """ Encapsulates a value in the cache in place of a normal value. """
3852+ def __init__(self, value):
3853+ self.value = value
3854+ def get(self):
3855+ return self.value
3856+
3857+class FailedValue(SpecialValue):
3858+ """ Special value that encapsulates an exception instead of a value. """
3859+ def __init__(self, exception):
3860+ self.exception = exception
3861+ def get(self):
3862+ raise self.exception
3863+
3864+def _check_value(value):
3865+ """ Return `value`, or call its getter if `value` is a :class:`SpecialValue`. """
3866+ return value.get() if isinstance(value, SpecialValue) else value
3867+
3868+
3869+def default(value):
3870+ """ Return a compute function that provides a constant default value. """
3871+ def compute(field, records):
3872+ for record in records:
3873+ record[field.name] = value
3874+
3875+ return compute
3876+
3877+
3878+def compute_related(field, records):
3879+ """ Compute the related `field` on `records`. """
3880+ scope = records._scope
3881+ sudo_scope = scope.sudo()
3882+ for record in records:
3883+ # bypass access rights check when traversing the related path
3884+ value = record.scoped(sudo_scope) if record.id else record
3885+ for name in field.related:
3886+ value = value[name]
3887+ # re-scope the resulting value
3888+ if isinstance(value, BaseModel):
3889+ value = value.scoped(scope)
3890+ record[field.name] = value
3891+
3892+def inverse_related(field, records):
3893+ """ Inverse the related `field` on `records`. """
3894+ for record in records:
3895+ other = record
3896+ for name in field.related[:-1]:
3897+ other = other[name]
3898+ if other:
3899+ other[field.related[-1]] = record[field.name]
3900+
3901+def search_related(field, operator, value):
3902+ """ Determine the domain to search on `field`. """
3903+ return [('.'.join(field.related), operator, value)]
3904+
3905+
3906+class MetaField(type):
3907+ """ Metaclass for field classes. """
3908+ by_type = {}
3909+
3910+ def __init__(cls, name, bases, attrs):
3911+ super(MetaField, cls).__init__(name, bases, attrs)
3912+ if cls.type:
3913+ cls.by_type[cls.type] = cls
3914+
3915+
3916+class Field(object):
3917+ """ Base class of all fields. """
3918+ __metaclass__ = MetaField
3919+
3920+ interface_for = None # the column or field interfaced by self, if any
3921+
3922+ name = None # name of the field
3923+ model_name = None # name of the model of this field
3924+ type = None # type of the field (string)
3925+ relational = False # whether the field is a relational one
3926+ inverse_field = None # inverse field (object), if it exists
3927+
3928+ store = True # whether the field is stored in database
3929+ depends = () # collection of field dependencies
3930+ compute = None # name of model method that computes value
3931+ inverse = None # name of model method that inverses field
3932+ search = None # name of model method that searches on field
3933+ related = None # sequence of field names, for related fields
3934+
3935+ string = None # field label
3936+ help = None # field tooltip
3937+ readonly = False
3938+ required = False
3939+ states = None
3940+ groups = False # csv list of group xml ids
3941+
3942+ # arguments passed to column class by to_column()
3943+ _column_string = attrgetter('string')
3944+ _column_help = attrgetter('help')
3945+ _column_readonly = attrgetter('readonly')
3946+ _column_required = attrgetter('required')
3947+ _column_states = attrgetter('states')
3948+ _column_groups = attrgetter('groups')
3949+
3950+ # attributes copied from related field by setup_related()
3951+ _related_string = attrgetter('string')
3952+ _related_help = attrgetter('help')
3953+ _related_readonly = attrgetter('readonly')
3954+ _related_required = attrgetter('required')
3955+ _related_states = attrgetter('states')
3956+ _related_groups = attrgetter('groups')
3957+
3958+ # attributes exported by get_description()
3959+ _description_depends = attrgetter('depends')
3960+ _description_related = attrgetter('related')
3961+ _description_string = attrgetter('string')
3962+ _description_help = attrgetter('help')
3963+ _description_readonly = attrgetter('readonly')
3964+ _description_required = attrgetter('required')
3965+ _description_states = attrgetter('states')
3966+ _description_groups = attrgetter('groups')
3967+
3968+ def __init__(self, string=None, **kwargs):
3969+ kwargs['string'] = string
3970+ for attr, value in kwargs.iteritems():
3971+ setattr(self, attr, value)
3972+
3973+ def reset(self):
3974+ """ Prepare `self` for a new setup. This resets all lazy properties. """
3975+ lazy_property.reset_all(self)
3976+
3977+ def copy(self, **kwargs):
3978+ """ make a copy of `self`, possibly modified with parameters `kwargs` """
3979+ field = copy(self)
3980+ for attr, value in kwargs.iteritems():
3981+ setattr(field, attr, value)
3982+ # Note: lazy properties will be recomputed later thanks to reset()
3983+ return field
3984+
3985+ def set_model_name(self, model_name, name):
3986+ """ assign the model and field names of `self` """
3987+ self.model_name = model_name
3988+ self.name = name
3989+ if not self.string:
3990+ self.string = name.replace('_', ' ').capitalize()
3991+
3992+ @lazy_property
3993+ def model(self):
3994+ """ return the model instance of `self` """
3995+ return scope[self.model_name]
3996+
3997+ def __str__(self):
3998+ return "%s.%s" % (self.model_name, self.name)
3999+
4000+ def get_description(self):
4001+ """ Return a dictionary that describes the field `self`. """
4002+ desc = {'type': self.type, 'store': self.store}
4003+ for attr in dir(self):
4004+ if attr.startswith('_description_'):
4005+ value = getattr(self, attr)(self)
4006+ if value:
4007+ desc[attr[13:]] = value
4008+ return desc
4009+
4010+ def to_column(self):
4011+ """ return a low-level field object corresponding to `self` """
4012+ assert self.store
4013+ if self.interface_for:
4014+ assert isinstance(self.interface_for, fields._column)
4015+ return self.interface_for
4016+
4017+ _logger.debug("Create fields._column for Field %s", self)
4018+ args = {}
4019+ for attr in dir(self):
4020+ if attr.startswith('_column_'):
4021+ args[attr[8:]] = getattr(self, attr)(self)
4022+ return getattr(fields, self.type)(**args)
4023+
4024+ #
4025+ # Conversion of values
4026+ #
4027+
4028+ def null(self):
4029+ """ return the null value for this field """
4030+ return False
4031+
4032+ def convert_to_cache(self, value):
4033+ """ convert `value` to the cache level; `value` may come from an
4034+ assignment, or have the format of methods :meth:`BaseModel.read` or
4035+ :meth:`BaseModel.write`
4036+ """
4037+ return value
4038+
4039+ def convert_to_read(self, value, use_name_get=True):
4040+ """ convert `value` from the cache to a value as returned by method
4041+ :meth:`BaseModel.read`
4042+ """
4043+ return value
4044+
4045+ def convert_to_write(self, value):
4046+ """ convert `value` from the cache to a valid value for method
4047+ :meth:`BaseModel.write`
4048+ """
4049+ return self.convert_to_read(value)
4050+
4051+ def convert_to_export(self, value):
4052+ """ convert `value` from the cache to a valid value for export. """
4053+ return bool(value) and ustr(value)
4054+
4055+ def convert_to_display_name(self, value):
4056+ """ convert `value` from the cache to a suitable display name. """
4057+ return ustr(value)
4058+
4059+ #
4060+ # Getter/setter methods
4061+ #
4062+
4063+ def __get__(self, record, owner):
4064+ """ return the value of field `self` on `record` """
4065+ if record is None:
4066+ return self # the field is accessed through the owner class
4067+
4068+ cache, id = record._scope.cache, record._id
4069+
4070+ try:
4071+ return _check_value(cache[self][id])
4072+ except KeyError:
4073+ pass
4074+
4075+ # cache miss, retrieve value
4076+ with record._scope:
4077+ if id:
4078+ # normal record -> read or compute value for this field
4079+ self.determine_value(record[0])
4080+ elif record:
4081+ # new record -> compute default value for this field
4082+ record.add_default_value(self.name)
4083+ else:
4084+ # null record -> return the null value for this field
4085+ return self.null()
4086+
4087+ # the result should be in cache now
4088+ return _check_value(cache[self][id])
4089+
4090+ def __set__(self, record, value):
4091+ """ set the value of field `self` on `record` """
4092+ if not record:
4093+ raise Warning("Null record %s may not be assigned" % record)
4094+
4095+ with record._scope as _scope:
4096+ # adapt value to the cache level
4097+ value = self.convert_to_cache(value)
4098+
4099+ # notify the change, which may cause cache invalidation
4100+ if _scope.draft or not record._id:
4101+ self.modified_draft(record)
4102+ else:
4103+ record.write({self.name: self.convert_to_write(value)})
4104+
4105+ # store the value in cache
4106+ _scope.cache[self][record._id] = value
4107+
4108+ #
4109+ # Management of the computation of field values.
4110+ #
4111+
4112+ @lazy_property
4113+ def _compute_function(self):
4114+ """ Return a function to call with records to compute this field. """
4115+ if isinstance(self.compute, basestring):
4116+ return getattr(type(self.model), self.compute)
4117+ elif callable(self.compute):
4118+ return partial(self.compute, self)
4119+ else:
4120+ raise Warning("No way to compute field %s" % self)
4121+
4122+ @lazy_property
4123+ def _compute_one(self):
4124+ """ Test whether the compute function has the decorator ``@one``. """
4125+ from openerp import one
4126+ return getattr(self._compute_function, '_api', None) is one
4127+
4128+ def compute_value(self, records, check_exists=False):
4129+ """ Invoke the compute method on `records`. If `check` is ``True``, the
4130+ method filters out non-existing records before computing them.
4131+ """
4132+ # if required, keep new and existing records only
4133+ if check_exists:
4134+ all_recs = records
4135+ new_recs = [rec for rec in records if not rec.id]
4136+ records = sum(new_recs, records.exists())
4137+
4138+ # mark non-existing records in cache
4139+ exc = MissingError("Computing a field on non-existing records.")
4140+ (all_recs - records)._update_cache(FailedValue(exc))
4141+
4142+ # mark the field failed in cache, so that access before computation
4143+ # raises an exception
4144+ exc = Warning("Field %s is accessed before being computed." % self)
4145+ records._update_cache({self.name: FailedValue(exc)})
4146+
4147+ self._compute_function(records)
4148+
4149+ def determine_value(self, record):
4150+ """ Determine the value of `self` for `record`. """
4151+ if self.store:
4152+ # recompute field on record if required
4153+ recs_todo = scope.recomputation.todo(self)
4154+ if record in recs_todo:
4155+ # execute the compute method in NON-DRAFT mode, so that assigned
4156+ # fields are written to the database
4157+ if self._compute_one:
4158+ self.compute_value(record, check_exists=True)
4159+ else:
4160+ self.compute_value(recs_todo, check_exists=True)
4161+ else:
4162+ record._prefetch_field(self.name)
4163+
4164+ else:
4165+ # execute the compute method in DRAFT mode, so that assigned fields
4166+ # are not written to the database
4167+ with record._scope.draft():
4168+ if self._compute_one:
4169+ self.compute_value(record)
4170+ else:
4171+ record._in_cache()
4172+ recs = record._in_cache_without(self.name)
4173+ self.compute_value(recs, check_exists=True)
4174+
4175+ def determine_default(self, record):
4176+ """ determine the default value of field `self` on `record` """
4177+ record._update_cache({self.name: SpecialValue(self.null())})
4178+ if self.compute:
4179+ self.compute_value(record)
4180+
4181+ def determine_inverse(self, records):
4182+ """ Given the value of `self` on `records`, inverse the computation. """
4183+ if isinstance(self.inverse, basestring):
4184+ getattr(records, self.inverse)()
4185+ elif callable(self.inverse):
4186+ self.inverse(self, records)
4187+
4188+ def determine_domain(self, operator, value):
4189+ """ Return a domain representing a condition on `self`. """
4190+ if isinstance(self.search, basestring):
4191+ return getattr(self.model.browse(), self.search)(operator, value)
4192+ elif callable(self.search):
4193+ return self.search(self, operator, value)
4194+ else:
4195+ return [(self.name, operator, value)]
4196+
4197+ #
4198+ # Setup of related fields.
4199+ #
4200+
4201+ @lazy_property
4202+ def related_field(self):
4203+ """ return the related field corresponding to `self` """
4204+ if self.related:
4205+ model = self.model
4206+ for name in self.related[:-1]:
4207+ model = model[name]
4208+ return model._fields[self.related[-1]]
4209+ return None
4210+
4211+ def setup_related(self):
4212+ """ Setup the attributes of the related field `self`. """
4213+ assert self.related
4214+ # fix the type of self.related if necessary
4215+ if isinstance(self.related, basestring):
4216+ self.related = tuple(self.related.split('.'))
4217+
4218+ # check type consistency
4219+ field = self.related_field
4220+ if self.type != field.type:
4221+ raise Warning("Type of related field %s is inconsistent with %s" % (self, field))
4222+
4223+ # determine dependencies, compute, inverse, and search
4224+ self.depends = ('.'.join(self.related),)
4225+ self.compute = compute_related
4226+ self.inverse = inverse_related
4227+ self.search = search_related
4228+
4229+ # copy attributes from field to self (readonly, required, etc.)
4230+ field.setup()
4231+ for attr in dir(self):
4232+ if attr.startswith('_related_'):
4233+ if not getattr(self, attr[9:]):
4234+ setattr(self, attr[9:], getattr(self, attr)(field))
4235+
4236+ #
4237+ # Field setup.
4238+ #
4239+ # Recomputation of computed fields: each field stores a set of triggers
4240+ # (`field`, `path`); when the field is modified, it invalidates the cache of
4241+ # `field` and registers the records to recompute based on `path`. See method
4242+ # `modified` below for details.
4243+ #
4244+
4245+ @lazy_property
4246+ def _triggers(self):
4247+ """ List of pairs (`field`, `path`), where `field` is a field to
4248+ recompute, and `path` is the dependency between `field` and `self`
4249+ (dot-separated sequence of field names between `field.model` and
4250+ `self.model`).
4251+ """
4252+ return []
4253+
4254+ def setup(self):
4255+ """ Complete the setup of `self`: make it process its dependencies and
4256+ store triggers on other fields to be recomputed.
4257+ """
4258+ return self._setup # trigger _setup() if not done yet
4259+
4260+ @lazy_property
4261+ def _setup(self):
4262+ if self.related:
4263+ # setup all attributes of related field
4264+ self.setup_related()
4265+ else:
4266+ # retrieve dependencies from compute method
4267+ if isinstance(self.compute, basestring):
4268+ method = getattr(type(self.model), self.compute)
4269+ else:
4270+ method = self.compute
4271+
4272+ depends = getattr(method, '_depends', ())
4273+ self.depends = depends(self.model) if callable(depends) else depends
4274+
4275+ # put invalidation/recomputation triggers on dependencies
4276+ for path in self.depends:
4277+ self._depends_on_model(self.model, [], path.split('.'))
4278+
4279+ def _depends_on_model(self, model, path0, path1):
4280+ """ Make `self` depend on `model`; `path0 + path1` is a dependency of
4281+ `self`, and `path0` is the sequence of field names from `self.model`
4282+ to `model`.
4283+ """
4284+ name, tail = path1[0], path1[1:]
4285+ if name == '*':
4286+ # special case: add triggers on all fields of model
4287+ fields = model._fields.values()
4288+ if not path0:
4289+ fields.remove(self) # self cannot depend directly on itself
4290+ else:
4291+ fields = (model._fields[name],)
4292+
4293+ for field in fields:
4294+ field._add_trigger_for(self, path0, tail)
4295+
4296+ def _add_trigger_for(self, field, path0, path1):
4297+ """ Add a trigger on `self` to recompute `field`; `path0` is the
4298+ sequence of field names from `field.model` to `self.model`; ``path0
4299+ + [self.name] + path1`` is a dependency of `field`.
4300+ """
4301+ self._triggers.append((field, '.'.join(path0) if path0 else 'id'))
4302+ _logger.debug("Add trigger on field %s to recompute field %s", self, field)
4303+
4304+ #
4305+ # Notification when fields are modified
4306+ #
4307+
4308+ def modified(self, records):
4309+ """ Notify that field `self` has been modified on `records`: prepare the
4310+ fields/records to recompute, and return a spec indicating what to
4311+ invalidate.
4312+ """
4313+ # invalidate cache for self
4314+ ids = records.unbrowse()
4315+ spec = [(self, ids)]
4316+
4317+ # invalidate the fields that depend on self, and prepare their
4318+ # recomputation
4319+ for field, path in self._triggers:
4320+ if field.store:
4321+ with scope(user=SUPERUSER_ID, context={'active_test': False}):
4322+ target = field.model.search([(path, 'in', ids)])
4323+ spec.append((field, target.unbrowse()))
4324+ scope.recomputation.todo(field, target)
4325+ else:
4326+ spec.append((field, None))
4327+
4328+ return spec
4329+
4330+ def modified_draft(self, records):
4331+ """ Same as :meth:`modified`, but in draft mode. """
4332+ # invalidate self and dependent fields on records only
4333+ model_name = self.model_name
4334+ fields = [self] + [f for f, _ in self._triggers if f.model_name == model_name]
4335+ ids = records._ids
4336+ scope.invalidate([(f, ids) for f in fields])
4337+
4338+
4339+class Boolean(Field):
4340+ """ Boolean field. """
4341+ type = 'boolean'
4342+
4343+ def convert_to_cache(self, value):
4344+ return bool(value)
4345+
4346+ def convert_to_export(self, value):
4347+ return ustr(value)
4348+
4349+
4350+class Integer(Field):
4351+ """ Integer field. """
4352+ type = 'integer'
4353+
4354+ def convert_to_cache(self, value):
4355+ return int(value or 0)
4356+
4357+
4358+class Float(Field):
4359+ """ Float field. """
4360+ type = 'float'
4361+ _digits = None
4362+
4363+ _column_digits = staticmethod(lambda self: not callable(self._digits) and self._digits)
4364+ _column_digits_compute = staticmethod(lambda self: callable(self._digits) and self._digits)
4365+
4366+ _related_digits = attrgetter('digits')
4367+ _description_digits = attrgetter('digits')
4368+
4369+ def __init__(self, string=None, digits=None, **kwargs):
4370+ self._digits = digits
4371+ super(Float, self).__init__(string=string, **kwargs)
4372+
4373+ @lazy_property
4374+ def digits(self):
4375+ return self._digits(scope.cr) if callable(self._digits) else self._digits
4376+
4377+ def convert_to_cache(self, value):
4378+ # apply rounding here, otherwise value in cache may be wrong!
4379+ if self.digits:
4380+ return float_round(float(value or 0.0), precision_digits=self.digits[1])
4381+ else:
4382+ return float(value or 0.0)
4383+
4384+
4385+class _String(Field):
4386+ """ Abstract class for string fields. """
4387+ translate = False
4388+
4389+ _column_translate = attrgetter('translate')
4390+ _related_translate = attrgetter('translate')
4391+ _description_translate = attrgetter('translate')
4392+
4393+
4394+class Char(_String):
4395+ """ Char field. """
4396+ type = 'char'
4397+ size = None
4398+
4399+ _column_size = attrgetter('size')
4400+ _related_size = attrgetter('size')
4401+ _description_size = attrgetter('size')
4402+
4403+ def convert_to_cache(self, value):
4404+ return bool(value) and ustr(value)[:self.size]
4405+
4406+
4407+class Text(_String):
4408+ """ Text field. """
4409+ type = 'text'
4410+
4411+ def convert_to_cache(self, value):
4412+ return bool(value) and ustr(value)
4413+
4414+
4415+class Html(_String):
4416+ """ Html field. """
4417+ type = 'html'
4418+
4419+ def convert_to_cache(self, value):
4420+ return bool(value) and html_sanitize(value)
4421+
4422+
4423+class Date(Field):
4424+ """ Date field. """
4425+ type = 'date'
4426+
4427+ def convert_to_cache(self, value):
4428+ if isinstance(value, (date, datetime)):
4429+ value = value.strftime(DATE_FORMAT)
4430+ elif value:
4431+ # check the date format
4432+ value = value[:DATE_LENGTH]
4433+ datetime.strptime(value, DATE_FORMAT)
4434+ return value or False
4435+
4436+
4437+class Datetime(Field):
4438+ """ Datetime field. """
4439+ type = 'datetime'
4440+
4441+ def convert_to_cache(self, value):
4442+ if isinstance(value, (date, datetime)):
4443+ value = value.strftime(DATETIME_FORMAT)
4444+ elif value:
4445+ # check the datetime format
4446+ value = value[:DATETIME_LENGTH]
4447+ datetime.strptime(value, DATETIME_FORMAT)
4448+ return value or False
4449+
4450+
4451+class Binary(Field):
4452+ """ Binary field. """
4453+ type = 'binary'
4454+
4455+
4456+class Selection(Field):
4457+ """ Selection field. """
4458+ type = 'selection'
4459+ selection = None # [(value, string), ...], model method or method name
4460+
4461+ _description_selection = staticmethod(lambda self: self.get_selection())
4462+
4463+ def __init__(self, selection, string=None, **kwargs):
4464+ """ Selection field.
4465+
4466+ :param selection: specifies the possible values for this field.
4467+ It is given as either a list of pairs (`value`, `string`), or a
4468+ model method, or a method name.
4469+ """
4470+ if callable(selection):
4471+ from openerp import api
4472+ selection = api.expected(api.model, selection)
4473+ super(Selection, self).__init__(selection=selection, string=string, **kwargs)
4474+
4475+ @staticmethod
4476+ def _column_selection(self):
4477+ if isinstance(self.selection, basestring):
4478+ method = self.selection
4479+ return lambda self, *a, **kw: getattr(self, method)(*a, **kw)
4480+ else:
4481+ return self.selection
4482+
4483+ def setup_related(self):
4484+ super(Selection, self).setup_related()
4485+ # selection must be computed on related field
4486+ self.selection = lambda model: self.related_field.get_selection()
4487+
4488+ def get_selection(self):
4489+ """ return the selection list (pairs (value, string)) """
4490+ value = self.selection
4491+ if isinstance(value, basestring):
4492+ value = getattr(self.model, value)()
4493+ elif callable(value):
4494+ value = value(self.model)
4495+ return value
4496+
4497+ def get_values(self):
4498+ """ return a list of the possible values """
4499+ return [item[0] for item in self.get_selection()]
4500+
4501+ def convert_to_cache(self, value):
4502+ if value in self.get_values():
4503+ return value
4504+ elif not value:
4505+ return False
4506+ raise ValueError("Wrong value for %s: %r" % (self, value))
4507+
4508+ def convert_to_export(self, value):
4509+ if not isinstance(self.selection, list):
4510+ # FIXME: this reproduces an existing buggy behavior!
4511+ return value
4512+ for item in self.get_selection():
4513+ if item[0] == value:
4514+ return item[1]
4515+ return False
4516+
4517+
4518+class Reference(Selection):
4519+ """ Reference field. """
4520+ type = 'reference'
4521+ size = 128
4522+
4523+ _column_size = attrgetter('size')
4524+ _related_size = attrgetter('size')
4525+
4526+ def __init__(self, selection, string=None, **kwargs):
4527+ """ Reference field.
4528+
4529+ :param selection: specifies the possible model names for this field.
4530+ It is given as either a list of pairs (`value`, `string`), or a
4531+ model method, or a method name.
4532+ """
4533+ super(Reference, self).__init__(selection=selection, string=string, **kwargs)
4534+
4535+ def convert_to_cache(self, value):
4536+ if isinstance(value, BaseModel):
4537+ if value._name in self.get_values() and len(value) <= 1:
4538+ return value.scoped() or False
4539+ elif isinstance(value, basestring):
4540+ res_model, res_id = value.split(',')
4541+ return scope[res_model].browse(int(res_id))
4542+ elif not value:
4543+ return False
4544+ raise ValueError("Wrong value for %s: %r" % (self, value))
4545+
4546+ def convert_to_read(self, value, use_name_get=True):
4547+ return "%s,%s" % (value._name, value.id) if value else False
4548+
4549+ def convert_to_export(self, value):
4550+ return bool(value) and value.name_get()[0][1]
4551+
4552+ def convert_to_display_name(self, value):
4553+ return ustr(value and value.display_name)
4554+
4555+
4556+class _Relational(Field):
4557+ """ Abstract class for relational fields. """
4558+ relational = True
4559+ comodel_name = None # name of model of values
4560+ domain = None # domain for searching values
4561+ context = None # context for searching values
4562+
4563+ _column_obj = attrgetter('comodel_name')
4564+ _column_domain = attrgetter('domain')
4565+ _column_context = attrgetter('context')
4566+
4567+ _description_relation = attrgetter('comodel_name')
4568+ _description_domain = staticmethod(lambda self: \
4569+ self.domain(self.model) if callable(self.domain) else self.domain)
4570+ _description_context = attrgetter('context')
4571+
4572+ def __init__(self, **kwargs):
4573+ super(_Relational, self).__init__(**kwargs)
4574+ if callable(self.domain):
4575+ from openerp import api
4576+ self.domain = api.expected(api.model, self.domain)
4577+
4578+ @lazy_property
4579+ def comodel(self):
4580+ """ return the comodel instance of `self` """
4581+ return scope[self.comodel_name]
4582+
4583+ def null(self):
4584+ return self.comodel.browse()
4585+
4586+ def _add_trigger_for(self, field, path0, path1):
4587+ # overridden to traverse relations and manage inverse fields
4588+ Field._add_trigger_for(self, field, path0, [])
4589+
4590+ if self.inverse_field:
4591+ # add trigger on inverse field, too
4592+ Field._add_trigger_for(self.inverse_field, field, path0 + [self.name], [])
4593+
4594+ if path1:
4595+ # recursively traverse the dependency
4596+ field._depends_on_model(self.comodel, path0 + [self.name], path1)
4597+
4598+ def modified(self, records):
4599+ # Invalidate cache for self.inverse_field, too. Note that recomputation
4600+ # of fields that depend on self.inverse_field is already covered by the
4601+ # triggers (see above).
4602+ spec = super(_Relational, self).modified(records)
4603+ if self.inverse_field:
4604+ spec.append((self.inverse_field, None))
4605+ return spec
4606+
4607+
4608+class Many2one(_Relational):
4609+ """ Many2one field. """
4610+ type = 'many2one'
4611+ ondelete = 'set null' # what to do when value is deleted
4612+ auto_join = False # whether joins are generated upon search
4613+ delegate = False # whether self implements delegation
4614+
4615+ _column_ondelete = attrgetter('ondelete')
4616+ _column_auto_join = attrgetter('auto_join')
4617+
4618+ def __init__(self, comodel_name, string=None, **kwargs):
4619+ super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
4620+
4621+ @lazy_property
4622+ def inverse_field(self):
4623+ for field in self.comodel._fields.itervalues():
4624+ if isinstance(field, One2many) and field.inverse_field == self:
4625+ return field
4626+ return None
4627+
4628+ @lazy_property
4629+ def inherits(self):
4630+ """ Whether `self` implements inheritance between model and comodel. """
4631+ return self.name in self.model._inherits.itervalues()
4632+
4633+ def convert_to_cache(self, value):
4634+ if isinstance(value, BaseModel):
4635+ if value._name == self.comodel_name and len(value) <= 1:
4636+ return value.scoped()
4637+ raise ValueError("Wrong value for %s: %r" % (self, value))
4638+ elif isinstance(value, tuple):
4639+ return self.comodel.browse(value[0])
4640+ elif isinstance(value, dict):
4641+ return self.comodel.new(value)
4642+ else:
4643+ return self.comodel.browse(value)
4644+
4645+ def convert_to_read(self, value, use_name_get=True):
4646+ if use_name_get and value:
4647+ # evaluate name_get() in sudo scope, because the visibility of a
4648+ # many2one field value (id and name) depends on the current record's
4649+ # access rights, and not the value's access rights.
4650+ with value._scope.sudo():
4651+ return value.name_get()[0]
4652+ else:
4653+ return value.id
4654+
4655+ def convert_to_write(self, value):
4656+ return value.id
4657+
4658+ def convert_to_export(self, value):
4659+ return bool(value) and value.name_get()[0][1]
4660+
4661+ def convert_to_display_name(self, value):
4662+ return ustr(value.display_name)
4663+
4664+ def determine_default(self, record):
4665+ super(Many2one, self).determine_default(record)
4666+ if self.inherits:
4667+ # special case: fields that implement inheritance between models
4668+ value = record[self.name]
4669+ if not value:
4670+ # the default value cannot be null, use a new record instead
4671+ record[self.name] = self.comodel.new()
4672+
4673+
4674+class _RelationalMulti(_Relational):
4675+ """ Abstract class for relational fields *2many. """
4676+
4677+ def convert_to_cache(self, value):
4678+ if isinstance(value, BaseModel):
4679+ if value._name == self.comodel_name:
4680+ return value.scoped()
4681+ elif isinstance(value, list):
4682+ # value is a list of record ids or commands
4683+ result = self.comodel.browse()
4684+ for command in value:
4685+ if isinstance(command, (tuple, list)):
4686+ if command[0] == 0:
4687+ result += self.comodel.new(command[2])
4688+ elif command[0] == 1:
4689+ record = self.comodel.browse(command[1])
4690+ record.update(command[2])
4691+ result += record
4692+ elif command[0] == 2:
4693+ pass
4694+ elif command[0] == 3:
4695+ pass
4696+ elif command[0] == 4:
4697+ result += self.comodel.browse(command[1])
4698+ elif command[0] == 5:
4699+ result = self.comodel.browse()
4700+ elif command[0] == 6:
4701+ result = self.comodel.browse(command[2])
4702+ elif isinstance(command, dict):
4703+ result += self.comodel.new(command)
4704+ else:
4705+ result += self.comodel.browse(command)
4706+ return result
4707+ elif not value:
4708+ return self.null()
4709+ raise ValueError("Wrong value for %s: %s" % (self, value))
4710+
4711+ def convert_to_read(self, value, use_name_get=True):
4712+ return value.unbrowse()
4713+
4714+ def convert_to_write(self, value):
4715+ result = [(5,)]
4716+ for record in value:
4717+ # TODO: modified record (1, id, values)
4718+ if not record.id:
4719+ values = record._convert_to_write(record._get_cache())
4720+ result.append((0, 0, values))
4721+ else:
4722+ result.append((4, record.id))
4723+ return result
4724+
4725+ def convert_to_export(self, value):
4726+ return bool(value) and ','.join(name for id, name in value.name_get())
4727+
4728+ def convert_to_display_name(self, value):
4729+ raise NotImplementedError()
4730+
4731+
4732+class One2many(_RelationalMulti):
4733+ """ One2many field. """
4734+ type = 'one2many'
4735+ inverse_name = None # name of the inverse field
4736+ auto_join = False # whether joins are generated upon search
4737+ limit = None # optional limit to use upon read
4738+
4739+ _column_fields_id = attrgetter('inverse_name')
4740+ _column_auto_join = attrgetter('auto_join')
4741+ _column_limit = attrgetter('limit')
4742+
4743+ _description_relation_field = attrgetter('inverse_name')
4744+
4745+ def __init__(self, comodel_name, inverse_name=None, string=None, **kwargs):
4746+ super(One2many, self).__init__(
4747+ comodel_name=comodel_name, inverse_name=inverse_name, string=string, **kwargs)
4748+
4749+ @lazy_property
4750+ def inverse_field(self):
4751+ return self.inverse_name and self.comodel._fields[self.inverse_name]
4752+
4753+
4754+class Many2many(_RelationalMulti):
4755+ """ Many2many field. """
4756+ type = 'many2many'
4757+ relation = None # name of table
4758+ column1 = None # column of table referring to model
4759+ column2 = None # column of table referring to comodel
4760+ limit = None # optional limit to use upon read
4761+
4762+ _column_rel = attrgetter('relation')
4763+ _column_id1 = attrgetter('column1')
4764+ _column_id2 = attrgetter('column2')
4765+ _column_limit = attrgetter('limit')
4766+
4767+ def __init__(self, comodel_name, relation=None, column1=None, column2=None,
4768+ string=None, **kwargs):
4769+ super(Many2many, self).__init__(comodel_name=comodel_name, relation=relation,
4770+ column1=column1, column2=column2, string=string, **kwargs)
4771+
4772+ @lazy_property
4773+ def inverse_field(self):
4774+ if not self.compute:
4775+ expected = (self.relation, self.column2, self.column1)
4776+ for field in self.comodel._fields.itervalues():
4777+ if isinstance(field, Many2many) and \
4778+ (field.relation, field.column1, field.column2) == expected:
4779+ return field
4780+ return None
4781+
4782+
4783+class Id(Field):
4784+ """ Special case for field 'id'. """
4785+ store = False
4786+ readonly = True
4787+
4788+ def to_column(self):
4789+ raise NotImplementedError()
4790+
4791+ def __get__(self, instance, owner):
4792+ if instance is None:
4793+ return self # the field is accessed through the class owner
4794+ return instance._id
4795+
4796+ def __set__(self, instance, value):
4797+ raise NotImplementedError()
4798+
4799+
4800+# imported here to avoid dependency cycle issues
4801+from openerp import SUPERUSER_ID
4802+from openerp.exceptions import Warning, MissingError
4803+from openerp.osv import fields
4804+from openerp.osv.orm import BaseModel
4805+from openerp.osv.scope import proxy as scope
4806
4807=== modified file 'openerp/osv/orm.py'
4808--- openerp/osv/orm.py 2014-01-17 12:02:35 +0000
4809+++ openerp/osv/orm.py 2014-01-22 16:19:54 +0000
4810@@ -21,46 +21,49 @@
4811
4812
4813 """
4814- Object relational mapping to database (postgresql) module
4815+ Object Relational Mapping module:
4816 * Hierarchical structure
4817- * Constraints consistency, validations
4818- * Object meta Data depends on its status
4819+ * Constraints consistency and validation
4820+ * Object metadata depends on its status
4821 * Optimised processing by complex query (multiple actions at once)
4822- * Default fields value
4823+ * Default field values
4824 * Permissions optimisation
4825 * Persistant object: DB postgresql
4826- * Datas conversions
4827+ * Data conversion
4828 * Multi-level caching system
4829- * 2 different inheritancies
4830- * Fields:
4831- - classicals (varchar, integer, boolean, ...)
4832- - relations (one2many, many2one, many2many)
4833- - functions
4834+ * Two different inheritance mechanisms
4835+ * Rich set of field types:
4836+ - classical (varchar, integer, boolean, ...)
4837+ - relational (one2many, many2one, many2many)
4838+ - functional
4839
4840 """
4841
4842 import calendar
4843-import collections
4844+from collections import defaultdict, Iterable
4845 import copy
4846 import datetime
4847 import itertools
4848 import logging
4849 import operator
4850 import pickle
4851+import functools
4852 import re
4853 import simplejson
4854 import time
4855-import traceback
4856-import types
4857
4858 import babel.dates
4859 import dateutil.parser
4860 import psycopg2
4861 from lxml import etree
4862
4863+import api
4864+from scope import proxy as scope_proxy
4865 import fields
4866 import openerp
4867 import openerp.tools as tools
4868+from openerp.exceptions import except_orm, AccessError, MissingError
4869+from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
4870 from openerp.tools.config import config
4871 from openerp.tools.misc import CountingStream
4872 from openerp.tools.safe_eval import safe_eval as eval
4873@@ -238,6 +241,10 @@
4874 def intersect(la, lb):
4875 return filter(lambda x: x in lb, la)
4876
4877+def same_name(f, g):
4878+ """ Test whether functions `f` and `g` are identical or have the same name """
4879+ return f == g or getattr(f, '__name__', 0) == getattr(g, '__name__', 1)
4880+
4881 def fix_import_export_id_paths(fieldname):
4882 """
4883 Fixes the id fields in import and exports, and splits field paths
4884@@ -251,303 +258,6 @@
4885 fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)
4886 return fixed_external_id.split('/')
4887
4888-class except_orm(Exception):
4889- def __init__(self, name, value):
4890- self.name = name
4891- self.value = value
4892- self.args = (name, value)
4893-
4894-class BrowseRecordError(Exception):
4895- pass
4896-
4897-class browse_null(object):
4898- """ Readonly python database object browser
4899- """
4900-
4901- def __init__(self):
4902- self.id = False
4903-
4904- def __getitem__(self, name):
4905- return None
4906-
4907- def __getattr__(self, name):
4908- return None # XXX: return self ?
4909-
4910- def __int__(self):
4911- return False
4912-
4913- def __str__(self):
4914- return ''
4915-
4916- def __nonzero__(self):
4917- return False
4918-
4919- def __unicode__(self):
4920- return u''
4921-
4922- def __iter__(self):
4923- raise NotImplementedError("Iteration is not allowed on %s" % self)
4924-
4925-
4926-#
4927-# TODO: execute an object method on browse_record_list
4928-#
4929-class browse_record_list(list):
4930- """ Collection of browse objects
4931-
4932- Such an instance will be returned when doing a ``browse([ids..])``
4933- and will be iterable, yielding browse() objects
4934- """
4935-
4936- def __init__(self, lst, context=None):
4937- if not context:
4938- context = {}
4939- super(browse_record_list, self).__init__(lst)
4940- self.context = context
4941-
4942-
4943-class browse_record(object):
4944- """ An object that behaves like a row of an object's table.
4945- It has attributes after the columns of the corresponding object.
4946-
4947- Examples::
4948-
4949- uobj = pool.get('res.users')
4950- user_rec = uobj.browse(cr, uid, 104)
4951- name = user_rec.name
4952- """
4953-
4954- def __init__(self, cr, uid, id, table, cache, context=None,
4955- list_class=browse_record_list, fields_process=None):
4956- """
4957- :param table: the browsed object (inherited from orm)
4958- :param dict cache: a dictionary of model->field->data to be shared
4959- across browse objects, thus reducing the SQL
4960- read()s. It can speed up things a lot, but also be
4961- disastrous if not discarded after write()/unlink()
4962- operations
4963- :param dict context: dictionary with an optional context
4964- """
4965- if fields_process is None:
4966- fields_process = {}
4967- if context is None:
4968- context = {}
4969- self._list_class = list_class
4970- self._cr = cr
4971- self._uid = uid
4972- self._id = id
4973- self._table = table # deprecated, use _model!
4974- self._model = table
4975- self._table_name = self._table._name
4976- self.__logger = logging.getLogger('openerp.osv.orm.browse_record.' + self._table_name)
4977- self._context = context
4978- self._fields_process = fields_process
4979-
4980- cache.setdefault(table._name, {})
4981- self._data = cache[table._name]
4982-
4983-# if not (id and isinstance(id, (int, long,))):
4984-# raise BrowseRecordError(_('Wrong ID for the browse record, got %r, expected an integer.') % (id,))
4985-# if not table.exists(cr, uid, id, context):
4986-# raise BrowseRecordError(_('Object %s does not exists') % (self,))
4987-
4988- if id not in self._data:
4989- self._data[id] = {'id': id}
4990-
4991- self._cache = cache
4992-
4993- def __getitem__(self, name):
4994- if name == 'id':
4995- return self._id
4996-
4997- if name not in self._data[self._id]:
4998- # build the list of fields we will fetch
4999-
5000- # fetch the definition of the field which was asked for
The diff has been truncated for viewing.