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
=== modified file 'doc/03_module_dev_02.rst'
--- doc/03_module_dev_02.rst 2013-06-19 09:13:32 +0000
+++ doc/03_module_dev_02.rst 2014-01-22 16:19:54 +0000
@@ -615,6 +615,7 @@
615 reference. :guilabel:`relation` is the table to look up that615 reference. :guilabel:`relation` is the table to look up that
616 reference in.616 reference in.
617617
618.. _fields-functional:
618619
619Functional Fields620Functional Fields
620+++++++++++++++++621+++++++++++++++++
621622
=== modified file 'doc/03_module_dev_03.rst'
--- doc/03_module_dev_03.rst 2013-09-04 12:58:42 +0000
+++ doc/03_module_dev_03.rst 2014-01-22 16:19:54 +0000
@@ -70,15 +70,21 @@
70On Change70On Change
71+++++++++71+++++++++
7272
73The on_change attribute defines a method that is called when the content of a view field has changed.73The on_change attribute defines a method that is called when the
74content of a view field has changed.
7475
75This 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::76This method takes at least arguments: cr, uid, ids, which are the
77three classical arguments and also the context dictionary. You can add
78parameters to the method. They must correspond to other fields defined
79in the view, and must also be defined in the XML with fields defined
80this way::
7681
77 <field name="name_of_field" on_change="name_of_method(other_field'_1_', ..., other_field'_n_')"/> 82 <field name="name_of_field" on_change="name_of_method(other_field'_1_', ..., other_field'_n_')"/>
7883
79The example below is from the sale order view.84The example below is from the sale order view.
8085
81You can use the 'context' keyword to access data in the context that can be used as params of the function.::86You can use the 'context' keyword to access data in the context that
87can be used as params of the function.::
8288
83 <field name="shop_id" on_change="onchange_shop_id(shop_id)"/>89 <field name="shop_id" on_change="onchange_shop_id(shop_id)"/>
8490
@@ -100,7 +106,10 @@
100 return {'value':v} 106 return {'value':v}
101107
102108
103When 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.109When editing the shop_id form field, the onchange_shop_id method of
110the sale_order object is called and returns a dictionary where the
111'value' key contains a dictionary of the new value to use in the
112'project_id', 'pricelist_id' and 'payment_default_id' fields.
104113
105Note that it is possible to change more than just the values of114Note that it is possible to change more than just the values of
106fields. For example, it is possible to change the value of some fields115fields. For example, it is possible to change the value of some fields
107116
=== modified file 'doc/api_models.rst'
--- doc/api_models.rst 2012-11-11 02:10:22 +0000
+++ doc/api_models.rst 2014-01-22 16:19:54 +0000
@@ -1,7 +1,21 @@
11
2ORM and models2ORM and Models
3--------------3==============
44
5.. automodule:: openerp.osv.orm5.. automodule:: openerp.osv.orm
6 :members:6 :members:
7 :undoc-members:7 :undoc-members:
8
9Scope Management
10================
11
12.. automodule:: openerp.osv.scope
13 :members:
14 :undoc-members:
15
16API Decorators
17==============
18
19.. automodule:: openerp.osv.api
20 :members:
21 :undoc-members:
822
=== modified file 'doc/index.rst'
--- doc/index.rst 2013-07-31 15:16:36 +0000
+++ doc/index.rst 2014-01-22 16:19:54 +0000
@@ -38,9 +38,10 @@
38.. toctree::38.. toctree::
39 :maxdepth: 139 :maxdepth: 1
4040
41 orm-methods.rst41 new_api
42 api_models.rst42 orm-methods
43 routing.rst43 api_models
44 routing
4445
45Changelog46Changelog
46'''''''''47'''''''''
4748
=== added file 'doc/new_api.rst'
--- doc/new_api.rst 1970-01-01 00:00:00 +0000
+++ doc/new_api.rst 2014-01-22 16:19:54 +0000
@@ -0,0 +1,138 @@
1==================
2High-level ORM API
3==================
4
5.. _compute:
6
7Computed fields: defaults and function fields
8=============================================
9
10The high-level API attempts to unify concepts of programmatic value generation
11for function fields (stored or not) and default values through the use of
12computed fields.
13
14Fields are marked as computed by setting their ``compute`` attribute to the
15name of the method used to compute then::
16
17 has_sibling = fields.Integer(compute='compute_has_sibling')
18
19by default computation methods behave as simple defaults in case no
20corresponding value is found in the database::
21
22 def default_number_of_employees(self):
23 self.number_of_employees = 1
24
25.. todo::
26
27 literal defaults::
28
29 has_sibling = fields.Integer(compute=fields.default(1))
30
31but they can also be used for computed fields by specifying fields used for
32the computation. The dependencies can be dotted for "cascading" through
33related models::
34
35 @api.depends('parent_id.children_count')
36 def compute_has_sibling(self):
37 self.has_sibling = self.parent_id.children_count >= 2
38
39.. todo::
40
41 function-based::
42
43 has_sibling = fields.Integer()
44 @has_sibling.computer
45 @api.depends('parent_id.children_count')
46 def compute_has_sibling(self):
47 self.has_sibling = self.parent_id.children_count >= 2
48
49note that computation methods (defaults or others) do not *return* a value,
50they *set* values the current object. This means the high-level API does not
51need :ref:`an explicit multi <fields-functional>`: a ``multi`` method is
52simply one which computes several values at once::
53
54 @api.depends('company_id')
55 def compute_relations(self):
56 self.computed_company = self.company_id
57 self.computed_companies = self.company_id.to_recordset()
58
59Automatic onchange
60==================
61
62Using to the improved and expanded :ref:`computed fields <compute>`, the
63high-level ORM API is able to infer the effect of fields on
64one another, and thus automatically provide a basic form of onchange without
65having to implement it by hand, or implement dozens of onchange functions to
66get everything right.
67
68
69
70
71.. todo::
72
73 deferred records::
74
75 partner = Partner.record(42, defer=True)
76 partner.name = "foo"
77 partner.user_id = juan
78 partner.save() # only saved to db here
79
80 with scope.defer():
81 # all records in this scope or children scopes are deferred
82 # until corresponding scope poped or until *this* scope poped?
83 partner = Partner.record(42)
84 partner.name = "foo"
85 partner.user_id = juan
86 # saved here, also for recordset &al, ~transaction
87
88 # temp deferment, maybe simpler? Or for bulk operations?:
89 with Partner.record(42) as partner:
90 partner.name = "foo"
91 partner.user_id = juan
92
93 ``id = False`` => always defered? null v draft?
94
95.. todo:: keyword arguments passed positionally (common for context, completely breaks everything)
96
97.. todo:: optional arguments (report_aged_receivable)
98
99.. todo:: non-id ids? (mail thread_id)
100
101.. todo:: partial signatures on overrides (e.g. message_post)
102
103.. todo::
104
105 ::
106
107 field = fields.Char()
108
109 @field.computer
110 def foo(self):
111 "compute foo here"
112
113 ~
114
115 ::
116
117 field = fields.Char(compute='foo')
118
119 def foo(self):
120 "compute foo here"
121
122.. todo:: doc
123
124.. todo:: incorrect dependency spec?
125
126.. todo:: dynamic dependencies?
127
128 ::
129
130 @api.depends(???)
131 def foo(self)
132 self.a = self[self.b]
133
134.. todo:: recursive onchange
135
136 Country & state. Change country -> remove state; set state -> set country
137
138.. todo:: onchange list affected?
0139
=== modified file 'openerp/__init__.py'
--- openerp/__init__.py 2013-09-09 23:19:46 +0000
+++ openerp/__init__.py 2014-01-22 16:19:54 +0000
@@ -58,6 +58,21 @@
58import sql_db58import sql_db
59import tools59import tools
60import workflow60import workflow
61
62# model classes
63from openerp.osv.orm import BaseModel, AbstractModel, Model, TransientModel
64
65# field classes
66from openerp.osv import fields2 as fields
67
68# api module and decorators
69from openerp.osv import api
70from openerp.osv.api import model, multi, one, constrains, depends, returns
71
72# scope proxy
73from openerp.osv.scope import proxy as scope
74
75
61# backward compatilbility76# backward compatilbility
62# TODO: This is for the web addons, can be removed later.77# TODO: This is for the web addons, can be removed later.
63wsgi = service78wsgi = service
6479
=== modified file 'openerp/addons/base/__openerp__.py'
--- openerp/addons/base/__openerp__.py 2013-10-06 15:18:27 +0000
+++ openerp/addons/base/__openerp__.py 2014-01-22 16:19:54 +0000
@@ -39,7 +39,6 @@
39 'res/res_country_data.xml',39 'res/res_country_data.xml',
40 'security/base_security.xml',40 'security/base_security.xml',
41 'base_menu.xml',41 'base_menu.xml',
42 'res/res_security.xml',
43 'res/res_config.xml',42 'res/res_config.xml',
44 'res/res.country.state.csv',43 'res/res.country.state.csv',
45 'ir/ir_actions.xml',44 'ir/ir_actions.xml',
@@ -81,7 +80,7 @@
81 'res/res_users_view.xml',80 'res/res_users_view.xml',
82 'res/res_partner_data.xml',81 'res/res_partner_data.xml',
83 'res/ir_property_view.xml',82 'res/ir_property_view.xml',
84 'security/base_security.xml',83 'res/res_security.xml',
85 'security/ir.model.access.csv',84 'security/ir.model.access.csv',
86 ],85 ],
87 'demo': [86 'demo': [
8887
=== modified file 'openerp/addons/base/base.sql'
--- openerp/addons/base/base.sql 2013-11-08 13:02:08 +0000
+++ openerp/addons/base/base.sql 2014-01-22 16:19:54 +0000
@@ -124,16 +124,6 @@
124 primary key(id)124 primary key(id)
125);125);
126126
127CREATE TABLE ir_ui_menu (
128 id serial NOT NULL,
129 parent_id int references ir_ui_menu on delete set null,
130 name varchar(64) DEFAULT ''::varchar NOT NULL,
131 icon varchar(64) DEFAULT ''::varchar,
132 primary key (id)
133);
134
135select setval('ir_ui_menu_id_seq', 2);
136
137---------------------------------127---------------------------------
138-- Res users128-- Res users
139---------------------------------129---------------------------------
@@ -171,7 +161,6 @@
171create index res_groups_users_rel_uid_idx on res_groups_users_rel (uid);161create index res_groups_users_rel_uid_idx on res_groups_users_rel (uid);
172create index res_groups_users_rel_gid_idx on res_groups_users_rel (gid);162create index res_groups_users_rel_gid_idx on res_groups_users_rel (gid);
173163
174
175---------------------------------164---------------------------------
176-- Workflows165-- Workflows
177---------------------------------166---------------------------------
@@ -264,10 +253,6 @@
264253
265CREATE TABLE ir_module_category (254CREATE TABLE ir_module_category (
266 id serial NOT NULL,255 id serial NOT NULL,
267 create_uid integer references res_users on delete set null,
268 create_date timestamp without time zone,
269 write_date timestamp without time zone,
270 write_uid integer references res_users on delete set null,
271 parent_id integer REFERENCES ir_module_category ON DELETE SET NULL,256 parent_id integer REFERENCES ir_module_category ON DELETE SET NULL,
272 name character varying(128) NOT NULL,257 name character varying(128) NOT NULL,
273 primary key(id)258 primary key(id)
@@ -276,10 +261,6 @@
276261
277CREATE TABLE ir_module_module (262CREATE TABLE ir_module_module (
278 id serial NOT NULL,263 id serial NOT NULL,
279 create_uid integer references res_users on delete set null,
280 create_date timestamp without time zone,
281 write_date timestamp without time zone,
282 write_uid integer references res_users on delete set null,
283 website character varying(256),264 website character varying(256),
284 summary character varying(256),265 summary character varying(256),
285 name character varying(128) NOT NULL,266 name character varying(128) NOT NULL,
@@ -304,10 +285,6 @@
304285
305CREATE TABLE ir_module_module_dependency (286CREATE TABLE ir_module_module_dependency (
306 id serial NOT NULL,287 id serial NOT NULL,
307 create_uid integer references res_users on delete set null,
308 create_date timestamp without time zone,
309 write_date timestamp without time zone,
310 write_uid integer references res_users on delete set null,
311 name character varying(128),288 name character varying(128),
312 version_pattern character varying(128) default NULL,289 version_pattern character varying(128) default NULL,
313 module_id integer REFERENCES ir_module_module ON DELETE cascade,290 module_id integer REFERENCES ir_module_module ON DELETE cascade,
@@ -345,10 +322,6 @@
345322
346CREATE TABLE ir_model_data (323CREATE TABLE ir_model_data (
347 id serial NOT NULL,324 id serial NOT NULL,
348 create_uid integer,
349 create_date timestamp without time zone,
350 write_date timestamp without time zone,
351 write_uid integer,
352 noupdate boolean,325 noupdate boolean,
353 name varchar NOT NULL,326 name varchar NOT NULL,
354 date_init timestamp without time zone,327 date_init timestamp without time zone,
@@ -364,10 +337,6 @@
364-- - for a constraint: type is 'u' (this is the convention PostgreSQL uses).337-- - for a constraint: type is 'u' (this is the convention PostgreSQL uses).
365CREATE TABLE ir_model_constraint (338CREATE TABLE ir_model_constraint (
366 id serial NOT NULL,339 id serial NOT NULL,
367 create_uid integer,
368 create_date timestamp without time zone,
369 write_date timestamp without time zone,
370 write_uid integer,
371 date_init timestamp without time zone,340 date_init timestamp without time zone,
372 date_update timestamp without time zone,341 date_update timestamp without time zone,
373 module integer NOT NULL references ir_module_module on delete restrict,342 module integer NOT NULL references ir_module_module on delete restrict,
@@ -380,10 +349,6 @@
380-- (so they can be removed when the module is uninstalled).349-- (so they can be removed when the module is uninstalled).
381CREATE TABLE ir_model_relation (350CREATE TABLE ir_model_relation (
382 id serial NOT NULL,351 id serial NOT NULL,
383 create_uid integer,
384 create_date timestamp without time zone,
385 write_date timestamp without time zone,
386 write_uid integer,
387 date_init timestamp without time zone,352 date_init timestamp without time zone,
388 date_update timestamp without time zone,353 date_update timestamp without time zone,
389 module integer NOT NULL references ir_module_module on delete restrict,354 module integer NOT NULL references ir_module_module on delete restrict,
@@ -409,4 +374,4 @@
409select setval('res_company_id_seq', 2);374select setval('res_company_id_seq', 2);
410select setval('res_users_id_seq', 2);375select setval('res_users_id_seq', 2);
411select setval('res_partner_id_seq', 2);376select setval('res_partner_id_seq', 2);
412select setval('res_currency_id_seq', 2);
413\ No newline at end of file377\ No newline at end of file
378select setval('res_currency_id_seq', 2);
414379
=== modified file 'openerp/addons/base/base_menu.xml'
--- openerp/addons/base/base_menu.xml 2013-10-06 11:26:08 +0000
+++ openerp/addons/base/base_menu.xml 2014-01-22 16:19:54 +0000
@@ -28,6 +28,10 @@
28 <menuitem id="menu_security" name="Security" parent="menu_custom" sequence="25"/>28 <menuitem id="menu_security" name="Security" parent="menu_custom" sequence="25"/>
29 <menuitem id="menu_ir_property" name="Parameters" parent="menu_custom" sequence="24"/>29 <menuitem id="menu_ir_property" name="Parameters" parent="menu_custom" sequence="24"/>
3030
31 <record model="ir.ui.menu" id="base.menu_administration">
32 <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
33 </record>
34
31 <record id="action_client_base_menu" model="ir.actions.client">35 <record id="action_client_base_menu" model="ir.actions.client">
32 <field name="name">Open Settings Menu</field>36 <field name="name">Open Settings Menu</field>
33 <field name="tag">reload</field>37 <field name="tag">reload</field>
3438
=== modified file 'openerp/addons/base/ir/ir_actions.py'
--- openerp/addons/base/ir/ir_actions.py 2014-01-15 20:53:57 +0000
+++ openerp/addons/base/ir/ir_actions.py 2014-01-22 16:19:54 +0000
@@ -324,7 +324,7 @@
324 dataobj = self.pool.get('ir.model.data')324 dataobj = self.pool.get('ir.model.data')
325 data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)325 data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id)
326 res_id = dataobj.browse(cr, uid, data_id, context).res_id326 res_id = dataobj.browse(cr, uid, data_id, context).res_id
327 return self.read(cr, uid, res_id, [], context)327 return self.read(cr, uid, [res_id], [], context)[0]
328328
329VIEW_TYPES = [329VIEW_TYPES = [
330 ('tree', 'Tree'),330 ('tree', 'Tree'),
@@ -544,7 +544,7 @@
544 'sequence': 5,544 'sequence': 5,
545 'code': """# You can use the following variables:545 'code': """# You can use the following variables:
546# - self: ORM model of the record on which the action is triggered546# - self: ORM model of the record on which the action is triggered
547# - object: browse_record of the record on which the action is triggered if there is one, otherwise None547# - object: Record on which the action is triggered if there is one, otherwise None
548# - pool: ORM model pool (i.e. self.pool)548# - pool: ORM model pool (i.e. self.pool)
549# - cr: database cursor549# - cr: database cursor
550# - uid: current user id550# - uid: current user id
@@ -799,7 +799,7 @@
799 def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):799 def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
800 if not action.action_id:800 if not action.action_id:
801 raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))801 raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
802 return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)802 return self.pool[action.action_id.type].read(cr, uid, [action.action_id.id], context=context)[0]
803803
804 def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):804 def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
805 eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'805 eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'
@@ -1038,10 +1038,10 @@
1038 wizard.write({'state': 'done'})1038 wizard.write({'state': 'done'})
10391039
1040 # Load action1040 # Load action
1041 act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context)1041 act_type = wizard.action_id.type
10421042
1043 res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context)1043 res = self.pool[act_type].read(cr, uid, [wizard.action_id.id], [], context=context)[0]
1044 if act_type['type'] != 'ir.actions.act_window':1044 if act_type != 'ir.actions.act_window':
1045 return res1045 return res
1046 res.setdefault('context','{}')1046 res.setdefault('context','{}')
1047 res['nodestroy'] = True1047 res['nodestroy'] = True
10481048
=== modified file 'openerp/addons/base/ir/ir_attachment.py'
--- openerp/addons/base/ir/ir_attachment.py 2014-01-07 14:15:24 +0000
+++ openerp/addons/base/ir/ir_attachment.py 2014-01-22 16:19:54 +0000
@@ -242,7 +242,7 @@
242 # performed in batch as much as possible.242 # performed in batch as much as possible.
243 ima = self.pool.get('ir.model.access')243 ima = self.pool.get('ir.model.access')
244 for model, targets in model_attachments.iteritems():244 for model, targets in model_attachments.iteritems():
245 if not self.pool.get(model):245 if model not in self.pool:
246 continue246 continue
247 if not ima.check(cr, uid, model, 'read', False):247 if not ima.check(cr, uid, model, 'read', False):
248 # remove all corresponding attachment ids248 # remove all corresponding attachment ids
@@ -266,7 +266,7 @@
266 if isinstance(ids, (int, long)):266 if isinstance(ids, (int, long)):
267 ids = [ids]267 ids = [ids]
268 self.check(cr, uid, ids, 'read', context=context)268 self.check(cr, uid, ids, 'read', context=context)
269 return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context, load)269 return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context=context, load=load)
270270
271 def write(self, cr, uid, ids, vals, context=None):271 def write(self, cr, uid, ids, vals, context=None):
272 if isinstance(ids, (int, long)):272 if isinstance(ids, (int, long)):
273273
=== modified file 'openerp/addons/base/ir/ir_cron.py'
--- openerp/addons/base/ir/ir_cron.py 2013-04-22 09:36:55 +0000
+++ openerp/addons/base/ir/ir_cron.py 2014-01-22 16:19:54 +0000
@@ -149,10 +149,10 @@
149 except Exception, e:149 except Exception, e:
150 self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)150 self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
151151
152 def _process_job(self, job_cr, job, cron_cr):152 def _process_job(self, cr, job, cron_cr):
153 """ Run a given job taking care of the repetition.153 """ Run a given job taking care of the repetition.
154154
155 :param job_cr: cursor to use to execute the job, safe to commit/rollback155 :param cr: cursor to use to execute the job, safe to commit/rollback
156 :param job: job to be run (as a dictionary).156 :param job: job to be run (as a dictionary).
157 :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,157 :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
158 must not be committed/rolled back!158 must not be committed/rolled back!
@@ -167,7 +167,7 @@
167 if numbercall > 0:167 if numbercall > 0:
168 numbercall -= 1168 numbercall -= 1
169 if not ok or job['doall']:169 if not ok or job['doall']:
170 self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])170 self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
171 if numbercall:171 if numbercall:
172 nextcall += _intervalTypes[job['interval_type']](job['interval_number'])172 nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
173 ok = True173 ok = True
@@ -176,9 +176,10 @@
176 addsql = ', active=False'176 addsql = ', active=False'
177 cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",177 cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
178 (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))178 (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
179 self.invalidate_cache(['nextcall', 'numbercall', 'active'], [job['id']])
179180
180 finally:181 finally:
181 job_cr.commit()182 cr.commit()
182 cron_cr.commit()183 cron_cr.commit()
183184
184 @classmethod185 @classmethod
185186
=== modified file 'openerp/addons/base/ir/ir_mail_server.py'
--- openerp/addons/base/ir/ir_mail_server.py 2013-11-23 11:30:53 +0000
+++ openerp/addons/base/ir/ir_mail_server.py 2014-01-22 16:19:54 +0000
@@ -461,21 +461,18 @@
461 mdir.add(message.as_string(True))461 mdir.add(message.as_string(True))
462 return message_id462 return message_id
463463
464 smtp = None
464 try:465 try:
465 smtp = self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption or False, smtp_debug)466 smtp = self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption or False, smtp_debug)
466 smtp.sendmail(smtp_from, smtp_to_list, message.as_string())467 smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
467 finally:468 finally:
468 try:469 if smtp is not None:
469 # Close Connection of SMTP Server
470 smtp.quit()470 smtp.quit()
471 except Exception:
472 # ignored, just a consequence of the previous exception
473 pass
474 except Exception, e:471 except Exception, e:
475 msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server),472 msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server),
476 e.__class__.__name__,473 e.__class__.__name__,
477 tools.ustr(e))474 tools.ustr(e))
478 _logger.exception(msg)475 _logger.error(msg)
479 raise MailDeliveryException(_("Mail Delivery Failed"), msg)476 raise MailDeliveryException(_("Mail Delivery Failed"), msg)
480 return message_id477 return message_id
481478
482479
=== modified file 'openerp/addons/base/ir/ir_model.py'
--- openerp/addons/base/ir/ir_model.py 2013-11-27 11:07:57 +0000
+++ openerp/addons/base/ir/ir_model.py 2014-01-22 16:19:54 +0000
@@ -28,12 +28,12 @@
28import openerp.modules.registry28import openerp.modules.registry
29from openerp import SUPERUSER_ID29from openerp import SUPERUSER_ID
30from openerp import tools30from openerp import tools
31from openerp.osv import fields,osv31from openerp.osv import fields, osv
32from openerp.osv.orm import Model32from openerp.osv.orm import BaseModel, Model, except_orm
33from openerp.osv.scope import proxy as scope
34from openerp.tools import config
33from openerp.tools.safe_eval import safe_eval as eval35from openerp.tools.safe_eval import safe_eval as eval
34from openerp.tools import config
35from openerp.tools.translate import _36from openerp.tools.translate import _
36from openerp.osv.orm import except_orm, browse_record
3737
38_logger = logging.getLogger(__name__)38_logger = logging.getLogger(__name__)
3939
@@ -133,15 +133,10 @@
133 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),133 ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
134 ]134 ]
135135
136 # overridden to allow searching both on model name (model field)136 def _search_display_name(self, operator, value):
137 # and model description (name field)137 # overridden to allow searching both on model name (model field) and
138 def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):138 # model description (name field)
139 if args is None:139 return ['|', ('model', operator, value), ('name', operator, value)]
140 args = []
141 domain = args + ['|', ('model', operator, name), ('name', operator, name)]
142 return self.name_get(cr, name_get_uid or uid,
143 super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
144 context=context)
145140
146 def _drop_table(self, cr, uid, ids, context=None):141 def _drop_table(self, cr, uid, ids, context=None):
147 for model in self.browse(cr, uid, ids, context):142 for model in self.browse(cr, uid, ids, context):
@@ -207,7 +202,8 @@
207 _custom = True202 _custom = True
208 x_custom_model._name = model203 x_custom_model._name = model
209 x_custom_model._module = False204 x_custom_model._module = False
210 a = x_custom_model.create_instance(self.pool, cr)205 with scope(cr, SUPERUSER_ID, None):
206 a = x_custom_model._build_model(self.pool, cr)
211 if not a._columns:207 if not a._columns:
212 x_name = 'id'208 x_name = 'id'
213 elif 'x_name' in a._columns.keys():209 elif 'x_name' in a._columns.keys():
@@ -625,8 +621,8 @@
625 """ Check if a specific group has the access mode to the specified model"""621 """ Check if a specific group has the access mode to the specified model"""
626 assert mode in ['read','write','create','unlink'], 'Invalid access mode'622 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
627623
628 if isinstance(model, browse_record):624 if isinstance(model, BaseModel):
629 assert model._table_name == 'ir.model', 'Invalid model object'625 assert model._name == 'ir.model', 'Invalid model object'
630 model_name = model.name626 model_name = model.name
631 else:627 else:
632 model_name = model628 model_name = model
@@ -684,8 +680,8 @@
684680
685 assert mode in ['read','write','create','unlink'], 'Invalid access mode'681 assert mode in ['read','write','create','unlink'], 'Invalid access mode'
686682
687 if isinstance(model, browse_record):683 if isinstance(model, BaseModel):
688 assert model._table_name == 'ir.model', 'Invalid model object'684 assert model._name == 'ir.model', 'Invalid model object'
689 model_name = model.model685 model_name = model.model
690 else:686 else:
691 model_name = model687 model_name = model
@@ -753,6 +749,7 @@
753 pass749 pass
754750
755 def call_cache_clearing_methods(self, cr):751 def call_cache_clearing_methods(self, cr):
752 scope.invalidate_all()
756 self.check.clear_cache(self) # clear the cache of check function753 self.check.clear_cache(self) # clear the cache of check function
757 for model, method in self.__cache_clearing_methods:754 for model, method in self.__cache_clearing_methods:
758 if model in self.pool:755 if model in self.pool:
@@ -761,19 +758,19 @@
761 #758 #
762 # Check rights on actions759 # Check rights on actions
763 #760 #
764 def write(self, cr, uid, *args, **argv):761 def write(self, cr, uid, ids, values, context=None):
765 self.call_cache_clearing_methods(cr)762 self.call_cache_clearing_methods(cr)
766 res = super(ir_model_access, self).write(cr, uid, *args, **argv)763 res = super(ir_model_access, self).write(cr, uid, ids, values, context=context)
767 return res764 return res
768765
769 def create(self, cr, uid, *args, **argv):766 def create(self, cr, uid, values, context=None):
770 self.call_cache_clearing_methods(cr)767 self.call_cache_clearing_methods(cr)
771 res = super(ir_model_access, self).create(cr, uid, *args, **argv)768 res = super(ir_model_access, self).create(cr, uid, values, context=context)
772 return res769 return res
773770
774 def unlink(self, cr, uid, *args, **argv):771 def unlink(self, cr, uid, ids, context=None):
775 self.call_cache_clearing_methods(cr)772 self.call_cache_clearing_methods(cr)
776 res = super(ir_model_access, self).unlink(cr, uid, *args, **argv)773 res = super(ir_model_access, self).unlink(cr, uid, ids, context=context)
777 return res774 return res
778775
779class ir_model_data(osv.osv):776class ir_model_data(osv.osv):
@@ -829,8 +826,8 @@
829 'date_init': fields.datetime('Init Date')826 'date_init': fields.datetime('Init Date')
830 }827 }
831 _defaults = {828 _defaults = {
832 'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),829 'date_init': fields.datetime.now,
833 'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),830 'date_update': fields.datetime.now,
834 'noupdate': False,831 'noupdate': False,
835 'module': ''832 'module': ''
836 }833 }
@@ -840,12 +837,11 @@
840837
841 def __init__(self, pool, cr):838 def __init__(self, pool, cr):
842 osv.osv.__init__(self, pool, cr)839 osv.osv.__init__(self, pool, cr)
843 self.doinit = True
844 # also stored in pool to avoid being discarded along with this osv instance840 # also stored in pool to avoid being discarded along with this osv instance
845 if getattr(pool, 'model_data_reference_ids', None) is None:841 if getattr(pool, 'model_data_reference_ids', None) is None:
846 self.pool.model_data_reference_ids = {}842 self.pool.model_data_reference_ids = {}
847843 # put loads on the class, in order to share it among all instances
848 self.loads = self.pool.model_data_reference_ids844 type(self).loads = self.pool.model_data_reference_ids
849845
850 def _auto_init(self, cr, context=None):846 def _auto_init(self, cr, context=None):
851 super(ir_model_data, self)._auto_init(cr, context)847 super(ir_model_data, self)._auto_init(cr, context)
@@ -867,7 +863,7 @@
867 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""863 """Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
868 data_id = self._get_id(cr, uid, module, xml_id)864 data_id = self._get_id(cr, uid, module, xml_id)
869 #assuming data_id is not False, as it was checked upstream865 #assuming data_id is not False, as it was checked upstream
870 res = self.read(cr, uid, data_id, ['model', 'res_id'])866 res = self.read(cr, uid, [data_id], ['model', 'res_id'])[0]
871 if not res['res_id']:867 if not res['res_id']:
872 raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))868 raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
873 return res['model'], res['res_id']869 return res['model'], res['res_id']
@@ -925,8 +921,6 @@
925 if xml_id and ('.' in xml_id):921 if xml_id and ('.' in xml_id):
926 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_id922 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
927 module, xml_id = xml_id.split('.')923 module, xml_id = xml_id.split('.')
928 if (not xml_id) and (not self.doinit):
929 return False
930 action_id = False924 action_id = False
931 if xml_id:925 if xml_id:
932 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model926 cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
@@ -996,8 +990,8 @@
996 if xml_id and res_id:990 if xml_id and res_id:
997 self.loads[(module, xml_id)] = (model, res_id)991 self.loads[(module, xml_id)] = (model, res_id)
998 for table, inherit_field in model_obj._inherits.iteritems():992 for table, inherit_field in model_obj._inherits.iteritems():
999 inherit_id = model_obj.read(cr, uid, res_id,993 inherit_id = model_obj.read(cr, uid, [res_id],
1000 [inherit_field])[inherit_field]994 [inherit_field])[0][inherit_field]
1001 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)995 self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id)
1002 return res_id996 return res_id
1003997
@@ -1020,11 +1014,12 @@
10201014
1021 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))1015 cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name))
1022 res = cr.fetchone()1016 res = cr.fetchone()
1017 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1023 if not res:1018 if not res:
1024 ir_values_obj = openerp.registry(cr.dbname)['ir.values']
1025 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)1019 ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta)
1026 elif xml_id:1020 elif xml_id:
1027 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))1021 cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name))
1022 ir_values_obj.invalidate_cache(['value'])
1028 return True1023 return True
10291024
1030 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):1025 def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
@@ -1066,6 +1061,7 @@
1066 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,))1061 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,))
1067 wkf_todo.extend(cr.fetchall())1062 wkf_todo.extend(cr.fetchall())
1068 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))1063 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))
1064 self.pool.get('workflow.transition').invalidate_cache()
10691065
1070 for model,res_id in wkf_todo:1066 for model,res_id in wkf_todo:
1071 try:1067 try:
10721068
=== modified file 'openerp/addons/base/ir/ir_rule.py'
--- openerp/addons/base/ir/ir_rule.py 2013-03-29 14:07:23 +0000
+++ openerp/addons/base/ir/ir_rule.py 2014-01-22 16:19:54 +0000
@@ -78,7 +78,7 @@
78 '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"),78 '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"),
79 'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'),79 'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'),
80 'domain_force': fields.text('Domain'),80 'domain_force': fields.text('Domain'),
81 'domain': fields.function(_domain_force_get, string='Domain', type='text'),81 'domain': fields.function(_domain_force_get, string='Domain', type='binary'),
82 'perm_read': fields.boolean('Apply for Read'),82 'perm_read': fields.boolean('Apply for Read'),
83 'perm_write': fields.boolean('Apply for Write'),83 'perm_write': fields.boolean('Apply for Write'),
84 'perm_create': fields.boolean('Apply for Create'),84 'perm_create': fields.boolean('Apply for Create'),
@@ -127,7 +127,7 @@
127 group_domains = {} # map: group -> list of domains127 group_domains = {} # map: group -> list of domains
128 for rule in self.browse(cr, SUPERUSER_ID, rule_ids):128 for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
129 # read 'domain' as UID to have the correct eval context for the rule.129 # read 'domain' as UID to have the correct eval context for the rule.
130 rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']130 rule_domain = self.read(cr, uid, [rule.id], ['domain'])[0]['domain']
131 dom = expression.normalize_domain(rule_domain)131 dom = expression.normalize_domain(rule_domain)
132 for group in rule.groups:132 for group in rule.groups:
133 if group in user.groups_id:133 if group in user.groups_id:
134134
=== modified file 'openerp/addons/base/ir/ir_sequence.py'
--- openerp/addons/base/ir/ir_sequence.py 2013-08-23 09:56:35 +0000
+++ openerp/addons/base/ir/ir_sequence.py 2014-01-22 16:19:54 +0000
@@ -234,15 +234,15 @@
234 'sec': time.strftime('%S', t),234 'sec': time.strftime('%S', t),
235 }235 }
236236
237 def _next(self, cr, uid, seq_ids, context=None):237 def _next(self, cr, uid, ids, context=None):
238 if not seq_ids:238 if not ids:
239 return False239 return False
240 if context is None:240 if context is None:
241 context = {}241 context = {}
242 force_company = context.get('force_company')242 force_company = context.get('force_company')
243 if not force_company:243 if not force_company:
244 force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id244 force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
245 sequences = self.read(cr, uid, seq_ids, ['name','company_id','implementation','number_next','prefix','suffix','padding'])245 sequences = self.read(cr, uid, ids, ['name','company_id','implementation','number_next','prefix','suffix','padding'])
246 preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ]246 preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ]
247 seq = preferred_sequences[0] if preferred_sequences else sequences[0]247 seq = preferred_sequences[0] if preferred_sequences else sequences[0]
248 if seq['implementation'] == 'standard':248 if seq['implementation'] == 'standard':
@@ -251,6 +251,7 @@
251 else:251 else:
252 cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],))252 cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],))
253 cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],))253 cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],))
254 self.invalidate_cache(['number_next'], [seq['id']])
254 d = self._interpolation_dict()255 d = self._interpolation_dict()
255 try:256 try:
256 interpolated_prefix = self._interpolate(seq['prefix'], d)257 interpolated_prefix = self._interpolate(seq['prefix'], d)
257258
=== modified file 'openerp/addons/base/ir/ir_translation.py'
--- openerp/addons/base/ir/ir_translation.py 2014-01-15 20:53:57 +0000
+++ openerp/addons/base/ir/ir_translation.py 2014-01-22 16:19:54 +0000
@@ -168,11 +168,11 @@
168 else:168 else:
169 model_name, field = record.name.split(',')169 model_name, field = record.name.split(',')
170 model = self.pool.get(model_name)170 model = self.pool.get(model_name)
171 if model and model.exists(cr, uid, record.res_id, context=context):171 if model is not None:
172 # Pass context without lang, need to read real stored field, not translation172 # Pass context without lang, need to read real stored field, not translation
173 context_no_lang = dict(context, lang=None)173 context_no_lang = dict(context, lang=None)
174 result = model.read(cr, uid, record.res_id, [field], context=context_no_lang)174 result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang)
175 res[record.id] = result[field] if result else False175 res[record.id] = result[0][field] if result else False
176 return res176 return res
177177
178 def _set_src(self, cr, uid, id, name, value, args, context=None):178 def _set_src(self, cr, uid, id, name, value, args, context=None):
179179
=== modified file 'openerp/addons/base/ir/ir_ui_menu.py'
--- openerp/addons/base/ir/ir_ui_menu.py 2013-10-06 13:24:24 +0000
+++ openerp/addons/base/ir/ir_ui_menu.py 2014-01-22 16:19:54 +0000
@@ -36,46 +36,48 @@
36 _name = 'ir.ui.menu'36 _name = 'ir.ui.menu'
3737
38 def __init__(self, *args, **kwargs):38 def __init__(self, *args, **kwargs):
39 self.cache_lock = threading.RLock()39 cls = type(self)
40 self._cache = {}40 cls._menu_cache_lock = threading.RLock()
41 cls._menu_cache = {}
41 super(ir_ui_menu, self).__init__(*args, **kwargs)42 super(ir_ui_menu, self).__init__(*args, **kwargs)
42 self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')43 self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
4344
44 def clear_cache(self):45 def clear_cache(self):
45 with self.cache_lock:46 with self._menu_cache_lock:
46 # radical but this doesn't frequently happen47 # radical but this doesn't frequently happen
47 if self._cache:48 if self._menu_cache:
48 # Normally this is done by openerp.tools.ormcache49 # Normally this is done by openerp.tools.ormcache
49 # but since we do not use it, set it by ourself.50 # but since we do not use it, set it by ourself.
50 self.pool._any_cache_cleared = True51 self.pool._any_cache_cleared = True
51 self._cache = {}52 self._menu_cache.clear()
5253
53 def _filter_visible_menus(self, cr, uid, ids, context=None):54 def _filter_visible_menus(self, cr, uid, ids, context=None):
54 """Filters the give menu ids to only keep the menu items that should be55 """Filters the give menu ids to only keep the menu items that should be
55 visible in the menu hierarchy of the current user.56 visible in the menu hierarchy of the current user.
56 Uses a cache for speeding up the computation.57 Uses a cache for speeding up the computation.
57 """58 """
58 with self.cache_lock:59 with self._menu_cache_lock:
59 modelaccess = self.pool.get('ir.model.access')60 modelaccess = self.pool.get('ir.model.access')
60 user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])61 user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
62 user_groups = set(user.groups_id.unbrowse())
61 result = []63 result = []
62 for menu in self.browse(cr, uid, ids, context=context):64 for menu in self.browse(cr, uid, ids, context=context):
63 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)65 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
64 key = (cr.dbname, menu.id, tuple(user_groups))66 key = (cr.dbname, menu.id, tuple(user_groups))
65 if key in self._cache:67 if key in self._menu_cache:
66 if self._cache[key]:68 if self._menu_cache[key]:
67 result.append(menu.id)69 result.append(menu.id)
68 #elif not menu.groups_id and not menu.action:70 #elif not menu.groups_id and not menu.action:
69 # result.append(menu.id)71 # result.append(menu.id)
70 continue72 continue
7173
72 self._cache[key] = False74 self._menu_cache[key] = False
73 if menu.groups_id:75 if menu.groups_id:
74 restrict_to_groups = [g.id for g in menu.groups_id]76 restrict_to_groups = [g.id for g in menu.groups_id]
75 if not user_groups.intersection(restrict_to_groups):77 if not user_groups.intersection(restrict_to_groups):
76 continue78 continue
77 #result.append(menu.id)79 #result.append(menu.id)
78 #self._cache[key] = True80 #self._menu_cache[key] = True
79 #continue81 #continue
8082
81 if menu.action:83 if menu.action:
@@ -99,7 +101,7 @@
99 continue101 continue
100102
101 result.append(menu.id)103 result.append(menu.id)
102 self._cache[key] = True104 self._menu_cache[key] = True
103 return result105 return result
104106
105 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):107 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
@@ -153,13 +155,13 @@
153 parent_path = ''155 parent_path = ''
154 return parent_path + elmt.name156 return parent_path + elmt.name
155157
156 def create(self, *args, **kwargs):158 def create(self, cr, uid, values, context=None):
157 self.clear_cache()159 self.clear_cache()
158 return super(ir_ui_menu, self).create(*args, **kwargs)160 return super(ir_ui_menu, self).create(cr, uid, values, context=context)
159161
160 def write(self, *args, **kwargs):162 def write(self, cr, uid, ids, values, context=None):
161 self.clear_cache()163 self.clear_cache()
162 return super(ir_ui_menu, self).write(*args, **kwargs)164 return super(ir_ui_menu, self).write(cr, uid, ids, values, context=context)
163165
164 def unlink(self, cr, uid, ids, context=None):166 def unlink(self, cr, uid, ids, context=None):
165 # Detach children and promote them to top-level, because it would be unwise to167 # Detach children and promote them to top-level, because it would be unwise to
166168
=== modified file 'openerp/addons/base/ir/ir_ui_view.py'
--- openerp/addons/base/ir/ir_ui_view.py 2013-10-06 13:24:24 +0000
+++ openerp/addons/base/ir/ir_ui_view.py 2014-01-22 16:19:54 +0000
@@ -60,11 +60,8 @@
60 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag60 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
61 return result61 return result
6262
63 _columns = {63 def _valid_view_types(self, cr, uid, context=None):
64 'name': fields.char('View Name', required=True),64 return [
65 'model': fields.char('Object', size=64, required=True, select=True),
66 'priority': fields.integer('Sequence', required=True),
67 'type': fields.function(_type_field, type='selection', selection=[
68 ('tree','Tree'),65 ('tree','Tree'),
69 ('form','Form'),66 ('form','Form'),
70 ('mdx','mdx'),67 ('mdx','mdx'),
@@ -73,7 +70,16 @@
73 ('diagram','Diagram'),70 ('diagram','Diagram'),
74 ('gantt', 'Gantt'),71 ('gantt', 'Gantt'),
75 ('kanban', 'Kanban'),72 ('kanban', 'Kanban'),
76 ('search','Search')], string='View Type', required=True, select=True, store=True),73 ('search', 'Search'),
74 ]
75
76 _columns = {
77 'name': fields.char('View Name', required=True),
78 'model': fields.char('Object', size=64, required=True, select=True),
79 'priority': fields.integer('Sequence', required=True),
80 'type': fields.function(_type_field, type='selection',
81 selection=lambda self, *args, **kwargs: self._valid_view_types(*args, **kwargs),
82 string='View Type', required=True, select=True, store=True),
77 'arch': fields.text('View Architecture', required=True),83 'arch': fields.text('View Architecture', required=True),
78 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),84 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
79 'field_parent': fields.char('Child Field',size=64),85 'field_parent': fields.char('Child Field',size=64),
@@ -119,7 +125,7 @@
119 """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by125 """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
120 its inherited views, by rendering it using ``fields_view_get()``.126 its inherited views, by rendering it using ``fields_view_get()``.
121 127
122 @param browse_record view: view to validate128 @param Record view: view to validate
123 @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)129 @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
124 if no error occurred, else False. 130 if no error occurred, else False.
125 """131 """
@@ -252,7 +258,7 @@
252 _Destination_Field=node_key258 _Destination_Field=node_key
253 flag = True259 flag = True
254260
255 datas = _Model_Obj.read(cr, uid, id, [],context)261 datas = _Model_Obj.read(cr, uid, [id], context=context)[0]
256 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):262 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
257 if a[_Source_Field] or a[_Destination_Field]:263 if a[_Source_Field] or a[_Destination_Field]:
258 nodes_name.append((a['id'],a['name']))264 nodes_name.append((a['id'],a['name']))
259265
=== modified file 'openerp/addons/base/ir/ir_values.py'
--- openerp/addons/base/ir/ir_values.py 2013-06-13 17:39:00 +0000
+++ openerp/addons/base/ir/ir_values.py 2014-01-22 16:19:54 +0000
@@ -20,6 +20,7 @@
20##############################################################################20##############################################################################
21import pickle21import pickle
2222
23from openerp import tools
23from openerp.osv import osv, fields24from openerp.osv import osv, fields
24from openerp.osv.orm import except_orm25from openerp.osv.orm import except_orm
2526
@@ -188,6 +189,21 @@
188 if not cr.fetchone():189 if not cr.fetchone():
189 cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')190 cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
190191
192 def create(self, cr, uid, vals, context=None):
193 res = super(ir_values, self).create(cr, uid, vals, context=context)
194 self.get_defaults_dict.clear_cache(self)
195 return res
196
197 def write(self, cr, uid, ids, vals, context=None):
198 res = super(ir_values, self).write(cr, uid, ids, vals, context=context)
199 self.get_defaults_dict.clear_cache(self)
200 return res
201
202 def unlink(self, cr, uid, ids, context=None):
203 res = super(ir_values, self).unlink(cr, uid, ids, context=context)
204 self.get_defaults_dict.clear_cache(self)
205 return res
206
191 def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):207 def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):
192 """Defines a default value for the given model and field_name. Any previous208 """Defines a default value for the given model and field_name. Any previous
193 default for the same scope (model, field_name, value, for_all_users, company_id, condition)209 default for the same scope (model, field_name, value, for_all_users, company_id, condition)
@@ -319,6 +335,15 @@
319 (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))335 (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
320 return defaults.values()336 return defaults.values()
321337
338 # use ormcache: this is called a lot by BaseModel.add_default_value()!
339 @tools.ormcache(skiparg=2)
340 def get_defaults_dict(self, cr, uid, model, condition=False):
341 """ Returns a dictionary mapping field names with their corresponding
342 default value. This method simply improves the returned value of
343 :meth:`~.get_defaults`.
344 """
345 return dict((f, v) for i, f, v in self.get_defaults(cr, uid, model, condition))
346
322 def set_action(self, cr, uid, name, action_slot, model, action, res_id=False):347 def set_action(self, cr, uid, name, action_slot, model, action, res_id=False):
323 """Binds an the given action to the given model's action slot - for later348 """Binds an the given action to the given model's action slot - for later
324 retrieval via :meth:`~.get_actions`. Any existing binding of the same action349 retrieval via :meth:`~.get_actions`. Any existing binding of the same action
@@ -401,7 +426,7 @@
401 if field not in EXCLUDED_FIELDS]426 if field not in EXCLUDED_FIELDS]
402 # FIXME: needs cleanup427 # FIXME: needs cleanup
403 try:428 try:
404 action_def = self.pool[action_model].read(cr, uid, int(id), fields, context)429 action_def = self.pool[action_model].read(cr, uid, [int(id)], fields, context)[0]
405 if action_def:430 if action_def:
406 if action_model in ('ir.actions.report.xml','ir.actions.act_window',431 if action_model in ('ir.actions.report.xml','ir.actions.act_window',
407 'ir.actions.wizard'):432 'ir.actions.wizard'):
408433
=== modified file 'openerp/addons/base/module/module.py'
--- openerp/addons/base/module/module.py 2014-01-15 20:53:57 +0000
+++ openerp/addons/base/module/module.py 2014-01-22 16:19:54 +0000
@@ -45,7 +45,7 @@
45from openerp.modules.db import create_categories45from openerp.modules.db import create_categories
46from openerp.tools.parse_version import parse_version46from openerp.tools.parse_version import parse_version
47from openerp.tools.translate import _47from openerp.tools.translate import _
48from openerp.osv import fields, osv, orm48from openerp.osv import osv, orm, fields, fields2, api
4949
50_logger = logging.getLogger(__name__)50_logger = logging.getLogger(__name__)
5151
@@ -362,34 +362,41 @@
362 msg = _('Unable to process module "%s" because an external dependency is not met: %s')362 msg = _('Unable to process module "%s" because an external dependency is not met: %s')
363 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))363 raise orm.except_orm(_('Error'), msg % (module_name, e.args[0]))
364364
365 def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):365 @api.multi
366 def state_update(self, newstate, states_to_update, level=100):
366 if level < 1:367 if level < 1:
367 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))368 raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
369
370 # whether some modules are installed with demo data
368 demo = False371 demo = False
369 for module in self.browse(cr, uid, ids, context=context):372
370 mdemo = False373 for module in self:
374 # determine dependency modules to update/others
375 update_mods, ready_mods = self.browse(), self.browse()
371 for dep in module.dependencies_id:376 for dep in module.dependencies_id:
372 if dep.state == 'unknown':377 if dep.state == 'unknown':
373 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,))378 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,))
374 ids2 = self.search(cr, uid, [('name', '=', dep.name)])379 if dep.depend_id.state == newstate:
375 if dep.state != newstate:380 ready_mods += dep.depend_id
376 mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level - 1) or mdemo
377 else:381 else:
378 od = self.browse(cr, uid, ids2)[0]382 update_mods += dep.depend_id
379 mdemo = od.demo or mdemo383
380384 # update dependency modules that require it, and determine demo for module
385 update_demo = update_mods.state_update(newstate, states_to_update, level=level-1)
386 module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods)
387 demo = demo or module_demo
388
389 # check dependencies and update module itself
381 self.check_external_dependencies(module.name, newstate)390 self.check_external_dependencies(module.name, newstate)
382 if not module.dependencies_id:
383 mdemo = module.demo
384 if module.state in states_to_update:391 if module.state in states_to_update:
385 self.write(cr, uid, [module.id], {'state': newstate, 'demo': mdemo})392 module.write({'state': newstate, 'demo': module_demo})
386 demo = demo or mdemo393
387 return demo394 return demo
388395
389 def button_install(self, cr, uid, ids, context=None):396 def button_install(self, cr, uid, ids, context=None):
390397
391 # Mark the given modules to be installed.398 # Mark the given modules to be installed.
392 self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)399 self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context=context)
393400
394 # Mark (recursively) the newly satisfied modules to also be installed401 # Mark (recursively) the newly satisfied modules to also be installed
395402
@@ -515,7 +522,7 @@
515522
516 def button_upgrade(self, cr, uid, ids, context=None):523 def button_upgrade(self, cr, uid, ids, context=None):
517 depobj = self.pool.get('ir.module.module.dependency')524 depobj = self.pool.get('ir.module.module.dependency')
518 todo = self.browse(cr, uid, ids, context=context)525 todo = list(self.browse(cr, uid, ids, context=context))
519 self.update_list(cr, uid)526 self.update_list(cr, uid)
520527
521 i = 0528 i = 0
@@ -646,6 +653,7 @@
646 terp = self.get_module_info(mod.name)653 terp = self.get_module_info(mod.name)
647 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))654 self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
648 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,))655 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,))
656 self.invalidate_cache(['dependencies_id'], [mod.id])
649 self._update_dependencies(cr, uid, mod, terp.get('depends', []))657 self._update_dependencies(cr, uid, mod, terp.get('depends', []))
650 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))658 self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
651 # Import module659 # Import module
@@ -738,6 +746,7 @@
738 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))746 cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep))
739 for dep in (existing - needed):747 for dep in (existing - needed):
740 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))748 cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep))
749 self.invalidate_cache(['dependencies_id'], [mod_browse.id])
741750
742 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):751 def _update_category(self, cr, uid, mod_browse, category='Uncategorized'):
743 current_category = mod_browse.category_id752 current_category = mod_browse.category_id
@@ -766,37 +775,49 @@
766 if not mod.description:775 if not mod.description:
767 _logger.warning('module %s: description is empty !', mod.name)776 _logger.warning('module %s: description is empty !', mod.name)
768777
769class module_dependency(osv.osv):778
779DEP_STATES = [
780 ('uninstallable', 'Uninstallable'),
781 ('uninstalled', 'Not Installed'),
782 ('installed', 'Installed'),
783 ('to upgrade', 'To be upgraded'),
784 ('to remove', 'To be removed'),
785 ('to install', 'To be installed'),
786 ('unknown', 'Unknown'),
787]
788
789class module_dependency(osv.Model):
770 _name = "ir.module.module.dependency"790 _name = "ir.module.module.dependency"
771 _description = "Module dependency"791 _description = "Module dependency"
772792
773 def _state(self, cr, uid, ids, name, args, context=None):793 # the dependency name
774 result = {}794 name = fields2.Char(size=128)
775 mod_obj = self.pool.get('ir.module.module')795
776 for md in self.browse(cr, uid, ids):796 # the module that depends on it
777 ids = mod_obj.search(cr, uid, [('name', '=', md.name)])797 module_id = fields2.Many2one('ir.module.module', 'Module', ondelete='cascade')
778 if ids:798
779 result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']799 # the module corresponding to the dependency, and its status
780 else:800 depend_id = fields2.Many2one('ir.module.module', 'Dependency',
781 result[md.id] = 'unknown'801 compute='_compute_depend', readonly=True, store=False)
782 return result802 state = fields2.Selection(DEP_STATES, string='Status',
783803 compute='_compute_state', readonly=True, store=False)
784 _columns = {804
785 # The dependency name805 @api.multi
786 'name': fields.char('Name', size=128, select=True),806 @api.depends('name')
787807 def _compute_depend(self):
788 # The module that depends on it808 # retrieve all modules corresponding to the dependency names
789 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),809 names = list(set(dep.name for dep in self))
790810 mods = self.pool['ir.module.module'].search([('name', 'in', names)])
791 'state': fields.function(_state, type='selection', selection=[811
792 ('uninstallable', 'Uninstallable'),812 # index modules by name, and assign dependencies
793 ('uninstalled', 'Not Installed'),813 name_mod = dict((mod.name, mod) for mod in mods)
794 ('installed', 'Installed'),814 for dep in self:
795 ('to upgrade', 'To be upgraded'),815 dep.depend_id = name_mod.get(dep.name)
796 ('to remove', 'To be removed'),816
797 ('to install', 'To be installed'),817 @api.one
798 ('unknown', 'Unknown'),818 @api.depends('depend_id.state')
799 ], string='Status', readonly=True, select=True),819 def _compute_state(self):
800 }820 self.state = self.depend_id.state or 'unknown'
821
801822
802# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:823# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
803824
=== modified file 'openerp/addons/base/res/ir_property.py'
--- openerp/addons/base/res/ir_property.py 2013-06-24 08:57:31 +0000
+++ openerp/addons/base/res/ir_property.py 2014-01-22 16:19:54 +0000
@@ -21,8 +21,7 @@
2121
22import time22import time
2323
24from openerp.osv import osv, fields24from openerp.osv import osv, orm, fields
25from openerp.osv.orm import browse_record, browse_null
26from openerp.tools.misc import attrgetter25from openerp.tools.misc import attrgetter
2726
28# -------------------------------------------------------------------------27# -------------------------------------------------------------------------
@@ -97,7 +96,7 @@
97 raise osv.except_osv('Error', 'Invalid type')96 raise osv.except_osv('Error', 'Invalid type')
9897
99 if field == 'value_reference':98 if field == 'value_reference':
100 if isinstance(value, browse_record):99 if isinstance(value, orm.BaseModel):
101 value = '%s,%d' % (value._name, value.id)100 value = '%s,%d' % (value._name, value.id)
102 elif isinstance(value, (int, long)):101 elif isinstance(value, (int, long)):
103 field_id = values.get('fields_id')102 field_id = values.get('fields_id')
@@ -132,7 +131,7 @@
132 return record.value_binary131 return record.value_binary
133 elif record.type == 'many2one':132 elif record.type == 'many2one':
134 if not record.value_reference:133 if not record.value_reference:
135 return browse_null()134 return False
136 model, resource_id = record.value_reference.split(',')135 model, resource_id = record.value_reference.split(',')
137 return self.pool.get(model).browse(cr, uid, int(resource_id), context=context)136 return self.pool.get(model).browse(cr, uid, int(resource_id), context=context)
138 elif record.type == 'datetime':137 elif record.type == 'datetime':
139138
=== modified file 'openerp/addons/base/res/res_company.py'
--- openerp/addons/base/res/res_company.py 2013-12-09 15:14:54 +0000
+++ openerp/addons/base/res/res_company.py 2014-01-22 16:19:54 +0000
@@ -84,7 +84,7 @@
84 if company.partner_id:84 if company.partner_id:
85 address_data = part_obj.address_get(cr, openerp.SUPERUSER_ID, [company.partner_id.id], adr_pref=['default'])85 address_data = part_obj.address_get(cr, openerp.SUPERUSER_ID, [company.partner_id.id], adr_pref=['default'])
86 if address_data['default']:86 if address_data['default']:
87 address = part_obj.read(cr, openerp.SUPERUSER_ID, address_data['default'], field_names, context=context)87 address = part_obj.read(cr, openerp.SUPERUSER_ID, [address_data['default']], field_names, context=context)[0]
88 for field in field_names:88 for field in field_names:
89 result[company.id][field] = address[field] or False89 result[company.id][field] = address[field] or False
90 return result90 return result
@@ -175,6 +175,7 @@
175 res += '\n%s: %s' % (title, ', '.join(name for id, name in account_names))175 res += '\n%s: %s' % (title, ', '.join(name for id, name in account_names))
176176
177 return {'value': {'rml_footer': res, 'rml_footer_readonly': res}}177 return {'value': {'rml_footer': res, 'rml_footer_readonly': res}}
178
178 def onchange_state(self, cr, uid, ids, state_id, context=None):179 def onchange_state(self, cr, uid, ids, state_id, context=None):
179 if state_id:180 if state_id:
180 return {'value':{'country_id': self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id }}181 return {'value':{'country_id': self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id }}
181182
=== modified file 'openerp/addons/base/res/res_config.py'
--- openerp/addons/base/res/res_config.py 2013-12-02 11:00:30 +0000
+++ openerp/addons/base/res/res_config.py 2014-01-22 16:19:54 +0000
@@ -293,10 +293,10 @@
293 def _already_installed(self, cr, uid, context=None):293 def _already_installed(self, cr, uid, context=None):
294 """ For each module (boolean fields in a res.config.installer),294 """ For each module (boolean fields in a res.config.installer),
295 check if it's already installed (either 'to install', 'to upgrade'295 check if it's already installed (either 'to install', 'to upgrade'
296 or 'installed') and if it is return the module's browse_record296 or 'installed') and if it is return the module's record
297297
298 :returns: a list of all installed modules in this installer298 :returns: a list of all installed modules in this installer
299 :rtype: [browse_record]299 :rtype: recordset (collection of Record)
300 """300 """
301 modules = self.pool['ir.module.module']301 modules = self.pool['ir.module.module']
302302
303303
=== modified file 'openerp/addons/base/res/res_currency.py'
--- openerp/addons/base/res/res_currency.py 2013-11-04 13:46:18 +0000
+++ openerp/addons/base/res/res_currency.py 2014-01-22 16:19:54 +0000
@@ -22,6 +22,7 @@
22import re22import re
23import time23import time
2424
25from openerp import api, fields as fields2
25from openerp import tools26from openerp import tools
26from openerp.osv import fields, osv27from openerp.osv import fields, osv
27from openerp.tools import float_round, float_is_zero, float_compare28from openerp.tools import float_round, float_is_zero, float_compare
@@ -77,7 +78,6 @@
77 'rounding': fields.float('Rounding Factor', digits=(12,6)),78 'rounding': fields.float('Rounding Factor', digits=(12,6)),
78 'active': fields.boolean('Active'),79 'active': fields.boolean('Active'),
79 'company_id':fields.many2one('res.company', 'Company'),80 'company_id':fields.many2one('res.company', 'Company'),
80 'date': fields.date('Date'),
81 'base': fields.boolean('Base'),81 'base': fields.boolean('Base'),
82 '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.")82 '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.")
83 }83 }
@@ -109,19 +109,12 @@
109 ON res_currency109 ON res_currency
110 (name, (COALESCE(company_id,-1)))""")110 (name, (COALESCE(company_id,-1)))""")
111111
112 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):112 date = fields2.Date(compute='compute_date', store=True)
113 res = super(res_currency, self).read(cr, user, ids, fields, context, load)113
114 currency_rate_obj = self.pool.get('res.currency.rate')114 @api.one
115 values = res115 @api.depends('rate_ids.name')
116 if not isinstance(values, list):116 def compute_date(self):
117 values = [values]117 self.date = self.rate_ids.name
118 for r in values:
119 if r.__contains__('rate_ids'):
120 rates=r['rate_ids']
121 if rates:
122 currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
123 r['date'] = currency_date
124 return res
125118
126 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):119 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
127 if not args:120 if not args:
@@ -147,7 +140,7 @@
147 """Return ``amount`` rounded according to ``currency``'s140 """Return ``amount`` rounded according to ``currency``'s
148 rounding rules.141 rounding rules.
149142
150 :param browse_record currency: currency for which we are rounding143 :param Record currency: currency for which we are rounding
151 :param float amount: the amount to round144 :param float amount: the amount to round
152 :return: rounded float145 :return: rounded float
153 """146 """
@@ -165,7 +158,7 @@
165 they respectively round to 0.01 and 0.0, even though158 they respectively round to 0.01 and 0.0, even though
166 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.159 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
167160
168 :param browse_record currency: currency for which we are rounding161 :param Record currency: currency for which we are rounding
169 :param float amount1: first amount to compare162 :param float amount1: first amount to compare
170 :param float amount2: second amount to compare163 :param float amount2: second amount to compare
171 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,164 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
@@ -183,7 +176,7 @@
183 computing the difference, while the latter will round before, giving176 computing the difference, while the latter will round before, giving
184 different results for e.g. 0.006 and 0.002 at 2 digits precision.177 different results for e.g. 0.006 and 0.002 at 2 digits precision.
185178
186 :param browse_record currency: currency for which we are rounding179 :param Record currency: currency for which we are rounding
187 :param float amount: amount to compare with currency's zero180 :param float amount: amount to compare with currency's zero
188 """181 """
189 return float_is_zero(amount, precision_rounding=currency.rounding)182 return float_is_zero(amount, precision_rounding=currency.rounding)
@@ -255,7 +248,7 @@
255 '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"),248 '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"),
256 }249 }
257 _defaults = {250 _defaults = {
258 'name': lambda *a: time.strftime('%Y-%m-%d'),251 'name': lambda *a: time.strftime('%Y-%m-%d 00:00:00'),
259 }252 }
260 _order = "name desc"253 _order = "name desc"
261254
262255
=== modified file 'openerp/addons/base/res/res_partner.py'
--- openerp/addons/base/res/res_partner.py 2013-09-10 12:12:30 +0000
+++ openerp/addons/base/res/res_partner.py 2014-01-22 16:19:54 +0000
@@ -29,41 +29,44 @@
29from openerp import SUPERUSER_ID29from openerp import SUPERUSER_ID
30from openerp import tools30from openerp import tools
31from openerp.osv import osv, fields31from openerp.osv import osv, fields
32from openerp.osv.scope import proxy as scope
33from openerp.osv.api import model, multi, one, returns
32from openerp.tools.translate import _34from openerp.tools.translate import _
33from openerp.tools.yaml_import import is_comment35from openerp.tools.yaml_import import is_comment
3436
37ADDRESS_FORMAT_LAYOUTS = {
38 '%(city)s %(state_code)s\n%(zip)s': """
39 <div class="address_format">
40 <field name="city" placeholder="City" style="width: 50%%"/>
41 <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/>
42 <br/>
43 <field name="zip" placeholder="ZIP"/>
44 </div>
45 """,
46 '%(zip)s %(city)s': """
47 <div class="address_format">
48 <field name="zip" placeholder="ZIP" style="width: 40%%"/>
49 <field name="city" placeholder="City" style="width: 57%%"/>
50 <br/>
51 <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
52 </div>
53 """,
54 '%(city)s\n%(state_name)s\n%(zip)s': """
55 <div class="address_format">
56 <field name="city" placeholder="City"/>
57 <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
58 <field name="zip" placeholder="ZIP"/>
59 </div>
60 """
61}
62
63
35class format_address(object):64class format_address(object):
36 def fields_view_get_address(self, cr, uid, arch, context={}):65 @model
37 user_obj = self.pool['res.users']66 def fields_view_get_address(self, arch):
38 fmt = user_obj.browse(cr, SUPERUSER_ID, uid, context).company_id.country_id67 fmt = scope.user.company_id.country_id.address_format or ''
39 fmt = fmt and fmt.address_format68 for k, v in ADDRESS_FORMAT_LAYOUTS.items():
40 layouts = {69 if k in fmt:
41 '%(city)s %(state_code)s\n%(zip)s': """
42 <div class="address_format">
43 <field name="city" placeholder="City" style="width: 50%%"/>
44 <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/>
45 <br/>
46 <field name="zip" placeholder="ZIP"/>
47 </div>
48 """,
49 '%(zip)s %(city)s': """
50 <div class="address_format">
51 <field name="zip" placeholder="ZIP" style="width: 40%%"/>
52 <field name="city" placeholder="City" style="width: 57%%"/>
53 <br/>
54 <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
55 </div>
56 """,
57 '%(city)s\n%(state_name)s\n%(zip)s': """
58 <div class="address_format">
59 <field name="city" placeholder="City"/>
60 <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/>
61 <field name="zip" placeholder="ZIP"/>
62 </div>
63 """
64 }
65 for k,v in layouts.items():
66 if fmt and (k in fmt):
67 doc = etree.fromstring(arch)70 doc = etree.fromstring(arch)
68 for node in doc.xpath("//div[@class='address_format']"):71 for node in doc.xpath("//div[@class='address_format']"):
69 tree = etree.fromstring(v)72 tree = etree.fromstring(v)
@@ -73,53 +76,53 @@
73 return arch76 return arch
7477
7578
76def _tz_get(self,cr,uid, context=None):79@model
80def _tz_get(self):
77 # put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 108672881 # put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728
78 return [(tz,tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')]82 return [(tz,tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')]
7983
80class res_partner_category(osv.osv):84
85class res_partner_category(osv.Model):
8186
82 def name_get(self, cr, uid, ids, context=None):87 def name_get(self, cr, uid, ids, context=None):
83 """Return the categories' display name, including their direct88 """ Return the categories' display name, including their direct
84 parent by default.89 parent by default.
8590
86 :param dict context: the ``partner_category_display`` key can be91 If ``context['partner_category_display']`` is ``'short'``, the short
87 used to select the short version of the92 version of the category name (without the direct parent) is used.
88 category name (without the direct parent),93 The default is the long version.
89 when set to ``'short'``. The default is94 """
90 the long version."""95 if not isinstance(ids, list):
96 ids = [ids]
91 if context is None:97 if context is None:
92 context = {}98 context = {}
99
93 if context.get('partner_category_display') == 'short':100 if context.get('partner_category_display') == 'short':
94 return super(res_partner_category, self).name_get(cr, uid, ids, context=context)101 return super(res_partner_category, self).name_get(cr, uid, ids, context=context)
95 if isinstance(ids, (int, long)):102
96 ids = [ids]
97 reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context)
98 res = []103 res = []
99 for record in reads:104 for category in self.browse(cr, uid, ids, context=context):
100 name = record['name']105 names = []
101 if record['parent_id']:106 current = category
102 name = record['parent_id'][1] + ' / ' + name107 while current:
103 res.append((record['id'], name))108 names.append(current.name)
109 current = current.parent_id
110 res.append((category.id, ' / '.join(reversed(names))))
104 return res111 return res
105112
106 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):113 @model
107 if not args:114 def name_search(self, name, args=None, operator='ilike', limit=100):
108 args = []115 args = args or []
109 if not context:
110 context = {}
111 if name:116 if name:
112 # Be sure name_search is symetric to name_get117 # Be sure name_search is symetric to name_get
113 name = name.split(' / ')[-1]118 name = name.split(' / ')[-1]
114 ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)119 args = [('name', operator, name)] + args
115 else:120 categories = self.search(args, limit=limit)
116 ids = self.search(cr, uid, args, limit=limit, context=context)121 return categories.name_get()
117 return self.name_get(cr, uid, ids, context)122
118123 @multi
119124 def _name_get_fnc(self, field_name, arg):
120 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):125 return dict(self.name_get())
121 res = self.name_get(cr, uid, ids, context=context)
122 return dict(res)
123126
124 _description = 'Partner Tags'127 _description = 'Partner Tags'
125 _name = 'res.partner.category'128 _name = 'res.partner.category'
@@ -143,6 +146,7 @@
143 _parent_order = 'name'146 _parent_order = 'name'
144 _order = 'parent_left'147 _order = 'parent_left'
145148
149
146class res_partner_title(osv.osv):150class res_partner_title(osv.osv):
147 _name = 'res.partner.title'151 _name = 'res.partner.title'
148 _order = 'name'152 _order = 'name'
@@ -155,16 +159,17 @@
155 'domain': 'contact',159 'domain': 'contact',
156 }160 }
157161
158def _lang_get(self, cr, uid, context=None):162
159 lang_pool = self.pool['res.lang']163@model
160 ids = lang_pool.search(cr, uid, [], context=context)164def _lang_get(self):
161 res = lang_pool.read(cr, uid, ids, ['code', 'name'], context)165 languages = scope['res.lang'].search([])
162 return [(r['code'], r['name']) for r in res]166 return [(language.code, language.name) for language in languages]
163167
164# fields copy if 'use_parent_address' is checked168# fields copy if 'use_parent_address' is checked
165ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id')169ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id')
166170
167class res_partner(osv.osv, format_address):171
172class res_partner(osv.Model, format_address):
168 _description = 'Partner'173 _description = 'Partner'
169 _name = "res.partner"174 _name = "res.partner"
170175
@@ -174,26 +179,23 @@
174 res[partner.id] = self._display_address(cr, uid, partner, context=context)179 res[partner.id] = self._display_address(cr, uid, partner, context=context)
175 return res180 return res
176181
177 def _get_image(self, cr, uid, ids, name, args, context=None):182 @multi
178 result = dict.fromkeys(ids, False)183 def _get_tz_offset(self, name, args):
179 for obj in self.browse(cr, uid, ids, context=context):184 return dict(
180 result[obj.id] = tools.image_get_resized_images(obj.image)185 (p.id, datetime.datetime.now(pytz.timezone(p.tz or 'GMT')).strftime('%z'))
181 return result186 for p in self)
182187
183 def _get_tz_offset(self, cr, uid, ids, name, args, context=None):188 @multi
184 result = dict.fromkeys(ids, False)189 def _get_image(self, name, args):
185 for obj in self.browse(cr, uid, ids, context=context):190 return dict((p.id, tools.image_get_resized_images(p.image)) for p in self)
186 result[obj.id] = datetime.datetime.now(pytz.timezone(obj.tz or 'GMT')).strftime('%z')191
187 return result192 @one
188193 def _set_image(self, name, value, args):
189 def _set_image(self, cr, uid, id, name, value, args, context=None):194 return self.write({'image': tools.image_resize_image_big(value)})
190 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)195
191196 @multi
192 def _has_image(self, cr, uid, ids, name, args, context=None):197 def _has_image(self, name, args):
193 result = {}198 return dict((p.id, bool(p.image)) for p in self)
194 for obj in self.browse(cr, uid, ids, context=context):
195 result[obj.id] = obj.image != False
196 return result
197199
198 def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):200 def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None):
199 """ Returns the partner that is considered the commercial201 """ Returns the partner that is considered the commercial
@@ -302,16 +304,15 @@
302 'commercial_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers)304 'commercial_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers)
303 }305 }
304306
305 def _default_category(self, cr, uid, context=None):307 @model
306 if context is None:308 def _default_category(self):
307 context = {}309 category_id = scope.context.get('category_id', False)
308 if context.get('category_id'):310 return [category_id] if category_id else False
309 return [context['category_id']]
310 return False
311311
312 def _get_default_image(self, cr, uid, is_company, context=None, colorize=False):312 @model
313 img_path = openerp.modules.get_module_resource('base', 'static/src/img',313 def _get_default_image(self, is_company, colorize=False):
314 ('company_image.png' if is_company else 'avatar.png'))314 img_path = openerp.modules.get_module_resource(
315 'base', 'static/src/img', 'company_image.png' if is_company else 'avatar.png')
315 with open(img_path, 'rb') as f:316 with open(img_path, 'rb') as f:
316 image = f.read()317 image = f.read()
317318
@@ -329,13 +330,17 @@
329 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)330 res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
330 return res331 return res
331332
333 @model
334 def _default_company(self):
335 return scope['res.company']._company_default_get('res.partner')
336
332 _defaults = {337 _defaults = {
333 'active': True,338 'active': True,
334 'lang': lambda self, cr, uid, ctx: ctx.get('lang', 'en_US'),339 'lang': model(lambda self: scope.lang),
335 'tz': lambda self, cr, uid, ctx: ctx.get('tz', False),340 'tz': model(lambda self: scope.context.get('tz', False)),
336 'customer': True,341 'customer': True,
337 'category_id': _default_category,342 'category_id': _default_category,
338 'company_id': lambda self, cr, uid, ctx: self.pool['res.company']._company_default_get(cr, uid, 'res.partner', context=ctx),343 'company_id': _default_company,
339 'color': 0,344 'color': 0,
340 'is_company': False,345 'is_company': False,
341 'type': 'contact', # type 'default' is wildcard and thus inappropriate346 'type': 'contact', # type 'default' is wildcard and thus inappropriate
@@ -347,14 +352,13 @@
347 (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']),352 (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']),
348 ]353 ]
349354
350 def copy(self, cr, uid, id, default=None, context=None):355 @one
351 if default is None:356 def copy(self, default=None):
352 default = {}357 default = dict(default or {}, name=_('%s (copy)') % self.name)
353 name = self.read(cr, uid, [id], ['name'], context)[0]['name']358 return super(res_partner, self).copy(default)
354 default.update({'name': _('%s (copy)') % name})
355 return super(res_partner, self).copy(cr, uid, id, default, context)
356359
357 def onchange_type(self, cr, uid, ids, is_company, context=None):360 @multi
361 def onchange_type(self, is_company):
358 value = {}362 value = {}
359 value['title'] = False363 value['title'] = False
360 if is_company:364 if is_company:
@@ -384,10 +388,11 @@
384 result['value'] = {'use_parent_address': False}388 result['value'] = {'use_parent_address': False}
385 return result389 return result
386390
387 def onchange_state(self, cr, uid, ids, state_id, context=None):391 @multi
392 def onchange_state(self, state_id):
388 if state_id:393 if state_id:
389 country_id = self.pool['res.country.state'].browse(cr, uid, state_id, context).country_id.id394 state = scope['res.country.state'].browse(state_id)
390 return {'value':{'country_id':country_id}}395 return {'value': {'country_id': state.country_id.id}}
391 return {}396 return {}
392397
393 def _check_ean_key(self, cr, uid, ids, context=None):398 def _check_ean_key(self, cr, uid, ids, context=None):
@@ -505,30 +510,32 @@
505 if not parent.is_company:510 if not parent.is_company:
506 parent.write({'is_company': True})511 parent.write({'is_company': True})
507512
508 def write(self, cr, uid, ids, vals, context=None):513 @multi
509 if isinstance(ids, (int, long)):514 def write(self, vals):
510 ids = [ids]515 # res.partner must only allow to set the company_id of a partner if it
511 #res.partner must only allow to set the company_id of a partner if it516 # is the same as the company of all users that inherit from this partner
512 #is the same as the company of all users that inherit from this partner517 # (this is to allow the code from res_users to write to the partner!) or
513 #(this is to allow the code from res_users to write to the partner!) or518 # if setting the company_id to False (this is compatible with any user
514 #if setting the company_id to False (this is compatible with any user company)519 # company)
515 if vals.get('company_id'):520 if vals.get('company_id'):
516 for partner in self.browse(cr, uid, ids, context=context):521 company = self.pool['res.company'].browse(vals['company_id'])
522 for partner in self:
517 if partner.user_ids:523 if partner.user_ids:
518 user_companies = set([user.company_id.id for user in partner.user_ids])524 companies = set(user.company_id for user in partner.user_ids)
519 if len(user_companies) > 1 or vals['company_id'] not in user_companies:525 if len(companies) > 1 or company not in companies:
520 raise osv.except_osv(_("Warning"),_("You can not change the company as the partner/user has multiple user linked with different companies."))526 raise osv.except_osv(_("Warning"),_("You can not change the company as the partner/user has multiple user linked with different companies."))
521 result = super(res_partner,self).write(cr, uid, ids, vals, context=context)527
522 for partner in self.browse(cr, uid, ids, context=context):528 result = super(res_partner, self).write(vals)
523 self._fields_sync(cr, uid, partner, vals, context)529 for partner in self:
530 self._fields_sync(partner, vals)
524 return result531 return result
525532
526 def create(self, cr, uid, vals, context=None):533 @model
527 new_id = super(res_partner, self).create(cr, uid, vals, context=context)534 def create(self, vals):
528 partner = self.browse(cr, uid, new_id, context=context)535 partner = super(res_partner, self).create(vals)
529 self._fields_sync(cr, uid, partner, vals, context)536 self._fields_sync(partner, vals)
530 self._handle_first_contact_creation(cr, uid, partner, context)537 self._handle_first_contact_creation(partner)
531 return new_id538 return partner
532539
533 def open_commercial_entity(self, cr, uid, ids, context=None):540 def open_commercial_entity(self, cr, uid, ids, context=None):
534 """ Utility method used to add an "Open Company" button in partner views """541 """ Utility method used to add an "Open Company" button in partner views """
@@ -712,14 +719,11 @@
712 return False719 return False
713 return _('Partners: ')+self.pool['res.partner.category'].browse(cr, uid, context['category_id'], context).name720 return _('Partners: ')+self.pool['res.partner.category'].browse(cr, uid, context['category_id'], context).name
714721
715 def main_partner(self, cr, uid):722 @model
716 ''' Return the id of the main partner723 @returns('self')
717 '''724 def main_partner(self):
718 model_data = self.pool['ir.model.data']725 ''' Return the main partner '''
719 return model_data.browse(cr, uid,726 return scope.ref('base.main_partner')
720 model_data.search(cr, uid, [('module','=','base'),
721 ('name','=','main_partner')])[0],
722 ).res_id
723727
724 def _display_address(self, cr, uid, address, without_company=False, context=None):728 def _display_address(self, cr, uid, address, without_company=False, context=None):
725729
@@ -735,14 +739,14 @@
735739
736 # get the information that will be injected into the display format740 # get the information that will be injected into the display format
737 # get the address format741 # get the address format
738 address_format = address.country_id and address.country_id.address_format or \742 address_format = address.country_id.address_format or \
739 "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"743 "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"
740 args = {744 args = {
741 'state_code': address.state_id and address.state_id.code or '',745 'state_code': address.state_id.code or '',
742 'state_name': address.state_id and address.state_id.name or '',746 'state_name': address.state_id.name or '',
743 'country_code': address.country_id and address.country_id.code or '',747 'country_code': address.country_id.code or '',
744 'country_name': address.country_id and address.country_id.name or '',748 'country_name': address.country_id.name or '',
745 'company_name': address.parent_id and address.parent_id.name or '',749 'company_name': address.parent_id.name or '',
746 }750 }
747 for field in self._address_fields(cr, uid, context=context):751 for field in self._address_fields(cr, uid, context=context):
748 args[field] = getattr(address, field) or ''752 args[field] = getattr(address, field) or ''
749753
=== modified file 'openerp/addons/base/res/res_users.py'
--- openerp/addons/base/res/res_users.py 2014-01-15 20:53:57 +0000
+++ openerp/addons/base/res/res_users.py 2014-01-22 16:19:54 +0000
@@ -25,11 +25,10 @@
25from lxml.builder import E25from lxml.builder import E
2626
27import openerp27import openerp
28from openerp import SUPERUSER_ID28from openerp import SUPERUSER_ID, BaseModel
29from openerp import tools29from openerp import tools
30import openerp.exceptions30import openerp.exceptions
31from openerp.osv import fields,osv31from openerp.osv import fields, osv, api
32from openerp.osv.orm import browse_record
33from openerp.tools.translate import _32from openerp.tools.translate import _
3433
35_logger = logging.getLogger(__name__)34_logger = logging.getLogger(__name__)
@@ -206,9 +205,8 @@
206 def _get_company(self,cr, uid, context=None, uid2=False):205 def _get_company(self,cr, uid, context=None, uid2=False):
207 if not uid2:206 if not uid2:
208 uid2 = uid207 uid2 = uid
209 user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)208 user = self.pool['res.users'].browse(cr, uid, uid2, context)
210 company_id = user.get('company_id', False)209 return user.company_id.id
211 return company_id and company_id[0] or False
212210
213 def _get_companies(self, cr, uid, context=None):211 def _get_companies(self, cr, uid, context=None):
214 c = self._get_company(cr, uid, context)212 c = self._get_company(cr, uid, context)
@@ -247,7 +245,7 @@
247 'company_id': _get_company,245 'company_id': _get_company,
248 'company_ids': _get_companies,246 'company_ids': _get_companies,
249 'groups_id': _get_group,247 'groups_id': _get_group,
250 'image': lambda self, cr, uid, ctx={}: self.pool['res.partner']._get_default_image(cr, uid, False, ctx, colorize=True),248 'image': api.model(lambda self: self.pool['res.partner']._get_default_image(False, colorize=True)),
251 }249 }
252250
253 # User can write on a few of his own fields (but not his groups for example)251 # User can write on a few of his own fields (but not his groups for example)
@@ -295,7 +293,8 @@
295 break293 break
296 else:294 else:
297 if 'company_id' in values:295 if 'company_id' in values:
298 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):296 user = self.browse(cr, SUPERUSER_ID, uid, context=context)
297 if not (values['company_id'] in user.company_ids.unbrowse()):
299 del values['company_id']298 del values['company_id']
300 uid = 1 # safe fields only, so we write as super-user to bypass access rights299 uid = 1 # safe fields only, so we write as super-user to bypass access rights
301300
@@ -360,8 +359,8 @@
360 else:359 else:
361 context_key = False360 context_key = False
362 if context_key:361 if context_key:
363 res = getattr(user,k) or False362 res = getattr(user, k) or False
364 if isinstance(res, browse_record):363 if isinstance(res, BaseModel):
365 res = res.id364 res = res.id
366 result[context_key] = res or False365 result[context_key] = res or False
367 return result366 return result
@@ -383,7 +382,7 @@
383 if not res:382 if not res:
384 raise openerp.exceptions.AccessDenied()383 raise openerp.exceptions.AccessDenied()
385384
386 def login(self, db, login, password):385 def _login(self, db, login, password):
387 if not password:386 if not password:
388 return False387 return False
389 user_id = False388 user_id = False
@@ -412,6 +411,7 @@
412 try:411 try:
413 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)412 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
414 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))413 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
414 self.invalidate_cache(['login_date'], [user_id])
415 except Exception:415 except Exception:
416 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)416 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
417 except openerp.exceptions.AccessDenied:417 except openerp.exceptions.AccessDenied:
@@ -433,7 +433,7 @@
433 :param dict user_agent_env: environment dictionary describing any433 :param dict user_agent_env: environment dictionary describing any
434 relevant environment attributes434 relevant environment attributes
435 """435 """
436 uid = self.login(db, login, password)436 uid = self._login(db, login, password)
437 if uid == openerp.SUPERUSER_ID:437 if uid == openerp.SUPERUSER_ID:
438 # Successfully logged in as admin!438 # Successfully logged in as admin!
439 # Attempt to guess the web base url...439 # Attempt to guess the web base url...
@@ -710,7 +710,7 @@
710 def get_user_groups_view(self, cr, uid, context=None):710 def get_user_groups_view(self, cr, uid, context=None):
711 try:711 try:
712 view = self.pool['ir.model.data'].get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)712 view = self.pool['ir.model.data'].get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
713 assert view and view._table_name == 'ir.ui.view'713 assert view and view._name == 'ir.ui.view'
714 except Exception:714 except Exception:
715 view = False715 view = False
716 return view716 return view
717717
=== modified file 'openerp/addons/base/security/base_security.xml'
--- openerp/addons/base/security/base_security.xml 2013-06-10 15:41:39 +0000
+++ openerp/addons/base/security/base_security.xml 2014-01-22 16:19:54 +0000
@@ -41,11 +41,6 @@
41 <field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>41 <field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>
42 </record>42 </record>
4343
44 <!-- Set accesses to menu -->
45 <record model="ir.ui.menu" id="base.menu_administration">
46 <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
47 </record>
48
49 <record model="ir.rule" id="res_partner_rule">44 <record model="ir.rule" id="res_partner_rule">
50 <field name="name">res.partner company</field>45 <field name="name">res.partner company</field>
51 <field name="model_id" ref="model_res_partner"/>46 <field name="model_id" ref="model_res_partner"/>
5247
=== modified file 'openerp/addons/base/test/base_test.yml'
--- openerp/addons/base/test/base_test.yml 2013-03-20 13:22:38 +0000
+++ openerp/addons/base/test/base_test.yml 2014-01-22 16:19:54 +0000
@@ -277,7 +277,7 @@
277 rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01',277 rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01',
278 'rate': value,278 'rate': value,
279 'currency_id': currency.id})279 'currency_id': currency.id})
280 rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate']280 rate = res_currency_rate.read(cr, 1, [rate_id], ['rate'])[0]['rate']
281 assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected)281 assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected)
282 # res.currency.rate uses 6 digits of precision by default282 # res.currency.rate uses 6 digits of precision by default
283 try_roundtrip(2.6748955, 2.674896)283 try_roundtrip(2.6748955, 2.674896)
284284
=== modified file 'openerp/addons/base/test/test_ir_rule.yml'
--- openerp/addons/base/test/test_ir_rule.yml 2011-06-01 14:39:21 +0000
+++ openerp/addons/base/test/test_ir_rule.yml 2014-01-22 16:19:54 +0000
@@ -124,7 +124,7 @@
124 Modify the global rule on res_company which triggers a recursive check124 Modify the global rule on res_company which triggers a recursive check
125 of the rules on company.125 of the rules on company.
126-126-
127 !record {model: ir.rule, id: base.res_company_rule}:127 !record {model: ir.rule, id: res_company_rule}:
128 domain_force: "[('id','child_of',[user.company_id.id])]"128 domain_force: "[('id','child_of',[user.company_id.id])]"
129-129-
130 Read as demo user the partners (exercising the global company rule).130 Read as demo user the partners (exercising the global company rule).
131131
=== modified file 'openerp/addons/base/test/test_osv_expression.yml'
--- openerp/addons/base/test/test_osv_expression.yml 2014-01-08 15:27:22 +0000
+++ openerp/addons/base/test/test_osv_expression.yml 2014-01-22 16:19:54 +0000
@@ -83,7 +83,7 @@
83 Test one2many operator with False83 Test one2many operator with False
84-84-
85 !assert {model: res.partner, search: "[('child_ids', '=', False)]"}:85 !assert {model: res.partner, search: "[('child_ids', '=', False)]"}:
86 - child_ids in (False, None, [])86 - list(child_ids) == []
87-87-
88 Test many2many operator with empty search list88 Test many2many operator with empty search list
89-89-
@@ -92,7 +92,7 @@
92 Test many2many operator with False92 Test many2many operator with False
93-93-
94 !assert {model: res.partner, search: "[('category_id', '=', False)]"}:94 !assert {model: res.partner, search: "[('category_id', '=', False)]"}:
95 - category_id in (False, None, [])95 - list(category_id) == []
96-96-
97 Filtering on invalid value across x2many relationship should return an empty set97 Filtering on invalid value across x2many relationship should return an empty set
98-98-
9999
=== modified file 'openerp/cli/server.py'
--- openerp/cli/server.py 2014-01-09 09:32:58 +0000
+++ openerp/cli/server.py 2014-01-22 16:19:54 +0000
@@ -96,6 +96,7 @@
96 registry = openerp.modules.registry.RegistryManager.new(dbname, update_module=update_module)96 registry = openerp.modules.registry.RegistryManager.new(dbname, update_module=update_module)
97 except Exception:97 except Exception:
98 _logger.exception('Failed to initialize database `%s`.', dbname)98 _logger.exception('Failed to initialize database `%s`.', dbname)
99 openerp.tools.post_mortem()
99 return False100 return False
100 return registry._assertion_report.failures == 0101 return registry._assertion_report.failures == 0
101102
102103
=== modified file 'openerp/exceptions.py'
--- openerp/exceptions.py 2013-02-15 14:35:03 +0000
+++ openerp/exceptions.py 2014-01-22 16:19:54 +0000
@@ -28,6 +28,13 @@
28If you consider introducing new exceptions, check out the test_exceptions addon.28If you consider introducing new exceptions, check out the test_exceptions addon.
29"""29"""
3030
31# kept for backward compatibility
32class except_orm(Exception):
33 def __init__(self, name, value):
34 self.name = name
35 self.value = value
36 self.args = (name, value)
37
31class Warning(Exception):38class Warning(Exception):
32 pass39 pass
3340
@@ -47,8 +54,15 @@
47 super(AccessDenied, self).__init__('Access denied.')54 super(AccessDenied, self).__init__('Access denied.')
48 self.traceback = ('', '', '')55 self.traceback = ('', '', '')
4956
50class AccessError(Exception):57class AccessError(except_orm):
51 """ Access rights error. """58 """ Access rights error. """
59 def __init__(self, msg):
60 super(AccessError, self).__init__('AccessError', msg)
61
62class MissingError(except_orm):
63 """ Missing record(s). """
64 def __init__(self, msg):
65 super(MissingError, self).__init__('MissingError', msg)
5266
53class DeferredException(Exception):67class DeferredException(Exception):
54 """ Exception object holding a traceback for asynchronous reporting.68 """ Exception object holding a traceback for asynchronous reporting.
5569
=== modified file 'openerp/modules/loading.py'
--- openerp/modules/loading.py 2013-12-18 12:38:58 +0000
+++ openerp/modules/loading.py 2014-01-22 16:19:54 +0000
@@ -175,6 +175,7 @@
175 status['progress'] = (index + 0.75) / len(graph)175 status['progress'] = (index + 0.75) / len(graph)
176 _load_data(cr, module_name, idref, mode, kind='demo')176 _load_data(cr, module_name, idref, mode, kind='demo')
177 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))177 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
178 modobj.invalidate_cache(['demo'], [module_id])
178179
179 migrations.migrate_module(package, 'post')180 migrations.migrate_module(package, 'post')
180181
@@ -310,6 +311,7 @@
310 modobj.button_upgrade(cr, SUPERUSER_ID, ids)311 modobj.button_upgrade(cr, SUPERUSER_ID, ids)
311312
312 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))313 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
314 modobj.invalidate_cache(['state'])
313315
314316
315 # STEP 3: Load marked modules (skipping base which was done in STEP 1)317 # STEP 3: Load marked modules (skipping base which was done in STEP 1)
316318
=== modified file 'openerp/modules/module.py'
--- openerp/modules/module.py 2013-06-28 15:07:55 +0000
+++ openerp/modules/module.py 2014-01-22 16:19:54 +0000
@@ -28,6 +28,7 @@
28import types28import types
29import zipimport29import zipimport
3030
31import openerp
31import openerp.tools as tools32import openerp.tools as tools
32import openerp.tools.osutil as osutil33import openerp.tools.osutil as osutil
33from openerp.tools.safe_eval import safe_eval as eval34from openerp.tools.safe_eval import safe_eval as eval
@@ -310,22 +311,23 @@
310 TODO better explanation of _auto_init and init.311 TODO better explanation of _auto_init and init.
311312
312 """313 """
313 _logger.info('module %s: creating or updating database tables', module_name)314 with openerp.osv.scope.Scope(cr, openerp.SUPERUSER_ID, None):
314 todo = []315 _logger.info('module %s: creating or updating database tables', module_name)
315 for obj in obj_list:316 todo = []
316 result = obj._auto_init(cr, {'module': module_name})317 for obj in obj_list:
317 if result:318 result = obj._auto_init(cr, {'module': module_name})
318 todo += result319 if result:
319 if hasattr(obj, 'init'):320 todo += result
320 obj.init(cr)321 if hasattr(obj, 'init'):
321 cr.commit()322 obj.init(cr)
322 for obj in obj_list:323 cr.commit()
323 obj._auto_end(cr, {'module': module_name})324 for obj in obj_list:
324 cr.commit()325 obj._auto_end(cr, {'module': module_name})
325 todo.sort()326 cr.commit()
326 for t in todo:327 todo.sort(key=lambda x: x[0])
327 t[1](cr, *t[2])328 for t in todo:
328 cr.commit()329 t[1](cr, *t[2])
330 cr.commit()
329331
330def load_openerp_module(module_name):332def load_openerp_module(module_name):
331 """ Load an OpenERP module, if not already loaded.333 """ Load an OpenERP module, if not already loaded.
332334
=== modified file 'openerp/modules/registry.py'
--- openerp/modules/registry.py 2013-11-20 10:25:45 +0000
+++ openerp/modules/registry.py 2014-01-22 16:19:54 +0000
@@ -27,12 +27,13 @@
27import logging27import logging
28import threading28import threading
2929
30from openerp import SUPERUSER_ID
30import openerp.sql_db31import openerp.sql_db
31import openerp.osv.orm32import openerp.osv.orm
32import openerp.tools33import openerp.tools
33import openerp.modules.db34import openerp.modules.db
34import openerp.tools.config35import openerp.tools.config
35from openerp.tools import assertion_report36from openerp.tools import assertion_report, lazy_property
3637
37_logger = logging.getLogger(__name__)38_logger = logging.getLogger(__name__)
3839
@@ -49,6 +50,7 @@
49 self.models = {} # model name/model instance mapping50 self.models = {} # model name/model instance mapping
50 self._sql_error = {}51 self._sql_error = {}
51 self._store_function = {}52 self._store_function = {}
53 self._pure_function_fields = {} # {model: [field, ...], ...}
52 self._init = True54 self._init = True
53 self._init_parent = {}55 self._init_parent = {}
54 self._assertion_report = assertion_report.assertion_report()56 self._assertion_report = assertion_report.assertion_report()
@@ -94,14 +96,20 @@
94 """ Return an iterator over all model names. """96 """ Return an iterator over all model names. """
95 return iter(self.models)97 return iter(self.models)
9698
97 def __contains__(self, model_name):
98 """ Test whether the model with the given name exists. """
99 return model_name in self.models
100
101 def __getitem__(self, model_name):99 def __getitem__(self, model_name):
102 """ Return the model with the given name or raise KeyError if it doesn't exist."""100 """ Return the model with the given name or raise KeyError if it doesn't exist."""
103 return self.models[model_name]101 return self.models[model_name]
104102
103 @lazy_property
104 def pure_function_fields(self):
105 """ Return the list of pure function fields (field objects) """
106 fields = []
107 for mname, fnames in self._pure_function_fields.iteritems():
108 model_fields = self[mname]._fields
109 for fname in fnames:
110 fields.append(model_fields[fname])
111 return fields
112
105 def do_parent_store(self, cr):113 def do_parent_store(self, cr):
106 for o in self._init_parent:114 for o in self._init_parent:
107 self.get(o)._parent_store_compute(cr)115 self.get(o)._parent_store_compute(cr)
@@ -125,14 +133,26 @@
125133
126 """134 """
127 models_to_load = [] # need to preserve loading order135 models_to_load = [] # need to preserve loading order
128 # Instantiate registered classes (via the MetaModel automatic discovery136 lazy_property.reset_all(self)
129 # or via explicit constructor call), and add them to the pool.137
130 for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):138 with openerp.osv.scope.Scope(cr, SUPERUSER_ID, None):
131 # models register themselves in self.models139 # call hook before adding stuff in the registry
132 model = cls.create_instance(self, cr)140 for model in self.models.itervalues():
133 if model._name not in models_to_load:141 model._before_registry_update()
134 # avoid double-loading models whose declaration is split142
135 models_to_load.append(model._name)143 # Instantiate registered classes (via the MetaModel automatic discovery
144 # or via explicit constructor call), and add them to the pool.
145 for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
146 # models register themselves in self.models
147 model = cls._build_model(self, cr)
148 if model._name not in models_to_load:
149 # avoid double-loading models whose declaration is split
150 models_to_load.append(model._name)
151
152 # call hook after models have been instantiated
153 for model in self.models.itervalues():
154 model._after_registry_update()
155
136 return [self.models[m] for m in models_to_load]156 return [self.models[m] for m in models_to_load]
137157
138 def clear_caches(self):158 def clear_caches(self):
@@ -144,7 +164,7 @@
144 model.clear_caches()164 model.clear_caches()
145 # Special case for ir_ui_menu which does not use openerp.tools.ormcache.165 # Special case for ir_ui_menu which does not use openerp.tools.ormcache.
146 ir_ui_menu = self.models.get('ir.ui.menu')166 ir_ui_menu = self.models.get('ir.ui.menu')
147 if ir_ui_menu:167 if ir_ui_menu is not None:
148 ir_ui_menu.clear_cache()168 ir_ui_menu.clear_cache()
149169
150170
151171
=== modified file 'openerp/netsvc.py'
--- openerp/netsvc.py 2013-05-28 10:27:33 +0000
+++ openerp/netsvc.py 2014-01-22 16:19:54 +0000
@@ -258,16 +258,12 @@
258 raise258 raise
259 except openerp.exceptions.DeferredException, e:259 except openerp.exceptions.DeferredException, e:
260 _logger.exception(tools.exception_to_unicode(e))260 _logger.exception(tools.exception_to_unicode(e))
261 post_mortem(e.traceback)261 tools.post_mortem(e.traceback)
262 raise262 raise
263 except Exception, e:263 except Exception, e:
264 _logger.exception(tools.exception_to_unicode(e))264 _logger.exception(tools.exception_to_unicode(e))
265 post_mortem(sys.exc_info())265 tools.post_mortem()
266 raise266 raise
267267
268def post_mortem(info):
269 if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType):
270 import pdb
271 pdb.post_mortem(info[2])
272268
273# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:269# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
274270
=== modified file 'openerp/osv/__init__.py'
--- openerp/osv/__init__.py 2013-02-12 14:24:10 +0000
+++ openerp/osv/__init__.py 2014-01-22 16:19:54 +0000
@@ -19,9 +19,10 @@
19#19#
20##############################################################################20##############################################################################
2121
22import api
22import osv23import osv
24import scope
23import fields25import fields
2426import fields2
2527
26# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:28# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
27
2829
=== added file 'openerp/osv/api.py'
--- openerp/osv/api.py 1970-01-01 00:00:00 +0000
+++ openerp/osv/api.py 2014-01-22 16:19:54 +0000
@@ -0,0 +1,760 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22""" This module provides the elements for managing two different API styles,
23 namely the "traditional" and "record" styles.
24
25 In the "traditional" style, parameters like the database cursor, user id,
26 context dictionary and record ids (usually denoted as ``cr``, ``uid``,
27 ``context``, ``ids``) are passed explicitly to all methods. In the "record"
28 style, those parameters are hidden in an execution environment (a "scope")
29 and the methods only refer to model instances, which gives it a more object-
30 oriented feel.
31
32 For instance, the statements::
33
34 model = self.pool.get(MODEL)
35 ids = model.search(cr, uid, DOMAIN, context=context)
36 for rec in model.browse(cr, uid, ids, context=context):
37 print rec.name
38 model.write(cr, uid, ids, VALUES, context=context)
39
40 may also be written as::
41
42 with scope(cr, uid, context): # cr, uid, context introduced once
43 model = scope[MODEL] # scope proxies the current scope
44 recs = model.search(DOMAIN) # search returns a recordset
45 for rec in recs: # iterate over the records
46 print rec.name
47 recs.write(VALUES) # update all records in recs
48
49 Methods written in the "traditional" style are automatically decorated,
50 following some heuristics based on parameter names.
51"""
52
53__all__ = [
54 'Meta', 'guess', 'noguess',
55 'model', 'multi', 'one',
56 'cr', 'cr_context', 'cr_uid', 'cr_uid_context',
57 'cr_uid_id', 'cr_uid_id_context', 'cr_uid_ids', 'cr_uid_ids_context',
58 'constrains', 'depends', 'returns',
59]
60
61from functools import update_wrapper
62from inspect import getargspec
63import logging
64
65_logger = logging.getLogger(__name__)
66
67
68#
69# The following attributes are used, and reflected on wrapping methods:
70# - method._api: decorator function, used for re-applying decorator
71# - method._constrains: set by @constrains, specifies constraint dependencies
72# - method._depends: set by @depends, specifies compute dependencies
73# - method._returns: set by @returns, specifies return model
74# - method.clear_cache: set by @ormcache, used to clear the cache
75#
76# On wrapping method only:
77# - method._orig: original method
78#
79
80_WRAPPED_ATTRS = ('_api', '_constrains', '_depends', '_returns', 'clear_cache')
81
82
83class Meta(type):
84 """ Metaclass that automatically decorates traditional-style methods by
85 guessing their API. It also implements the inheritance of the
86 :func:`returns` decorators.
87 """
88
89 def __new__(meta, name, bases, attrs):
90 # dummy parent class to catch overridden methods decorated with 'returns'
91 parent = type.__new__(meta, name, bases, {})
92
93 for key, value in attrs.items():
94 if not key.startswith('__') and callable(value):
95 # make the method inherit from @returns decorators
96 if not _returns(value) and _returns(getattr(parent, key, None)):
97 value = returns(getattr(parent, key))(value)
98 _logger.debug("Method %s.%s inherited @returns%r",
99 name, value.__name__, _returns(value))
100
101 # guess calling convention if none is given
102 if not hasattr(value, '_api'):
103 value = guess(value)
104
105 attrs[key] = value
106
107 return type.__new__(meta, name, bases, attrs)
108
109
110def constrains(*args):
111 """ Return a decorator that specifies the field dependencies of a method
112 implementing a constraint checker. Each argument must be a field name.
113 """
114 def decorate(method):
115 method._constrains = args
116 return method
117
118 return decorate
119
120
121def depends(*args):
122 """ Return a decorator that specifies the field dependencies of a "compute"
123 method (for new-style function fields). Each argument must be a string
124 that consists in a dot-separated sequence of field names.
125
126 One may also pass a single function as argument. In that case, the
127 dependencies are given by calling the function with the field's model.
128 """
129 if args and callable(args[0]):
130 args = args[0]
131
132 def decorate(method):
133 method._depends = args
134 return method
135
136 return decorate
137
138
139def returns(model, traditional=None):
140 """ Return a decorator for methods that return instances of `model`.
141
142 :param model: a model name, ``'self'`` for the current model, or a method
143 (in which case the model is taken from that method's decorator)
144
145 :param traditional: a function `convert(self, value)` to convert the
146 record-style value to the traditional-style output
147
148 The decorator adapts the method output to the api style: `id`, `ids` or
149 ``False`` for the traditional style, and record, recordset or null for
150 the record style::
151
152 @model
153 @returns('res.partner')
154 def find_partner(self, arg):
155 ... # return some record
156
157 # output depends on call style: traditional vs record style
158 partner_id = model.find_partner(cr, uid, arg, context=context)
159 partner_record = model.find_partner(arg)
160
161 Note that the decorated method must satisfy that convention.
162
163 Those decorators are automatically *inherited*: a method that overrides
164 a decorated existing method will be decorated with the same
165 ``@returns(model)``.
166 """
167 if callable(model):
168 # model is a method, check its own @returns decoration
169 spec = _returns(model)
170 if not spec:
171 return lambda method: method
172 else:
173 spec = model, traditional
174
175 def decorate(method):
176 if hasattr(method, '_orig'):
177 # decorate the original method, and re-apply the api decorator
178 origin = method._orig
179 origin._returns = spec
180 return origin._api(origin)
181 else:
182 method._returns = spec
183 return method
184
185 return decorate
186
187
188def _returns(method):
189 return getattr(method, '_returns', None)
190
191
192# constant converters
193_CONVERT_PASS = lambda self, value: value
194_CONVERT_BROWSE = lambda self, value: self.browse(value)
195_CONVERT_UNBROWSE = lambda self, value: value.unbrowse()
196
197
198def _converter_to_old(method):
199 """ Return a function `convert(self, value)` that adapts `value` from
200 record-style to traditional-style returning convention of `method`.
201 """
202 spec = _returns(method)
203 if spec:
204 model, traditional = spec
205 return traditional or _CONVERT_UNBROWSE
206 else:
207 return _CONVERT_PASS
208
209
210def _converter_to_new(method):
211 """ Return a function `convert(self, value)` that adapts `value` from
212 traditional-style to record-style returning convention of `method`.
213 """
214 spec = _returns(method)
215 if spec:
216 model, traditional = spec
217 if model == 'self':
218 return _CONVERT_BROWSE
219 else:
220 return lambda self, value: self.pool[model].browse(value)
221 else:
222 return _CONVERT_PASS
223
224
225def _aggregator_one(method):
226 """ Return a function `convert(self, value)` that aggregates record-style
227 `value` for a method decorated with ``@one``.
228 """
229 spec = _returns(method)
230 if spec:
231 # value is a list of instances, concatenate them
232 model, traditional = spec
233 if model == 'self':
234 return lambda self, value: sum(value, self.browse())
235 else:
236 return lambda self, value: sum(value, self.pool[model].browse())
237 else:
238 return _CONVERT_PASS
239
240
241def wraps(method):
242 """ Return a decorator for an api wrapper of `method`. """
243 def decorate(wrapper):
244 # propagate '__module__', '__name__', '__doc__' to wrapper
245 update_wrapper(wrapper, method)
246 # propagate specific openerp attributes to wrapper
247 for attr in _WRAPPED_ATTRS:
248 if hasattr(method, attr):
249 setattr(wrapper, attr, getattr(method, attr))
250 wrapper._orig = method
251 return wrapper
252
253 return decorate
254
255
256def _has_cursor(args, kwargs):
257 """ test whether `args` or `kwargs` contain a cursor argument """
258 cr = args[0] if args else kwargs.get('cr')
259 return isinstance(cr, Cursor)
260
261
262def _cr_uid_context_splitter(method):
263 """ return a function that splits scope parameters from the other arguments """
264 names = getargspec(method).args[1:]
265 ctx_pos = len(names) + 2
266
267 def split(args, kwargs):
268 cr, uid = args[:2]
269 if ctx_pos < len(args):
270 return cr, uid, args[ctx_pos], args[2:ctx_pos], kwargs
271 else:
272 return cr, uid, kwargs.pop('context', None), args[2:], kwargs
273
274 return split
275
276
277def _cr_uid_ids_context_splitter(method):
278 """ return a function that splits scope parameters from the other arguments """
279 names = getargspec(method).args[1:]
280 ctx_pos = len(names) + 3
281
282 def split(args, kwargs):
283 cr, uid, ids = args[:3]
284 if ctx_pos < len(args):
285 return cr, uid, ids, args[ctx_pos], args[3:ctx_pos], kwargs
286 else:
287 return cr, uid, ids, kwargs.pop('context', None), args[3:], kwargs
288
289 return split
290
291
292def _scope_cr_getter(method):
293 """ return a function that makes the scope corresponding to parameters """
294 names = getargspec(method).args
295 cr_name = len(names) > 1 and names[1]
296
297 def get(args, kwargs):
298 cr = args[0] if args else kwargs[cr_name]
299 return Scope(cr, SUPERUSER_ID, None)
300
301 return get
302
303
304def _scope_cr_uid_getter(method):
305 """ return a function that makes the scope corresponding to parameters """
306 names = getargspec(method).args
307 cr_name = len(names) > 1 and names[1]
308 uid_name = len(names) > 2 and names[2]
309
310 def get(args, kwargs):
311 nargs = len(args)
312 cr = args[0] if nargs > 0 else kwargs[cr_name]
313 uid = args[1] if nargs > 1 else kwargs[uid_name]
314 return Scope(cr, uid, None)
315
316 return get
317
318
319def _scope_cr_context_getter(method):
320 """ return a function that makes the scope corresponding to parameters """
321 names = getargspec(method).args
322 cr_name = len(names) > 1 and names[1]
323 ctx_pos = names.index('context') - 1 if 'context' in names else 1024
324
325 def get(args, kwargs):
326 nargs = len(args)
327 cr = args[0] if nargs > 0 else kwargs[cr_name]
328 context = args[ctx_pos] if nargs > ctx_pos else kwargs.get('context')
329 return Scope(cr, SUPERUSER_ID, context)
330
331 return get
332
333
334def _scope_cr_uid_context_getter(method):
335 """ return a function that makes the scope corresponding to parameters """
336 names = getargspec(method).args
337 cr_name = len(names) > 1 and names[1]
338 uid_name = len(names) > 2 and names[2]
339 ctx_pos = names.index('context') - 1 if 'context' in names else 1024
340
341 def get(args, kwargs):
342 nargs = len(args)
343 cr = args[0] if nargs > 0 else kwargs[cr_name]
344 uid = args[1] if nargs > 1 else kwargs[uid_name]
345 context = args[ctx_pos] if nargs > ctx_pos else kwargs.get('context')
346 return Scope(cr, uid, context)
347
348 return get
349
350
351def model(method):
352 """ Decorate a record-style method where `self` is any instance with scope
353 (model, record or recordset). Such a method::
354
355 @api.model
356 def method(self, args):
357 ...
358
359 may be called in both record and traditional styles, like::
360
361 model.method(args)
362 model.method(cr, uid, args, context=context)
363 """
364 method._api = model
365 split_args = _cr_uid_context_splitter(method)
366 new_to_old = _converter_to_old(method)
367
368 @wraps(method)
369 def model_wrapper(self, *args, **kwargs):
370 if _has_cursor(args, kwargs):
371 cr, uid, context, args, kwargs = split_args(args, kwargs)
372 with Scope(cr, uid, context):
373 value = method(self, *args, **kwargs)
374 return new_to_old(self, value)
375 else:
376 return method(self, *args, **kwargs)
377
378 return model_wrapper
379
380
381def multi(method):
382 """ Decorate a record-style method where `self` is a recordset. Such a
383 method::
384
385 @api.multi
386 def method(self, args):
387 ...
388
389 may be called in both record and traditional styles, like::
390
391 recs = model.browse(ids)
392
393 # the following calls are equivalent
394 recs.method(args)
395 model.method(cr, uid, ids, args, context=context)
396 """
397 method._api = multi
398 split_args = _cr_uid_ids_context_splitter(method)
399 new_to_old = _converter_to_old(method)
400
401 @wraps(method)
402 def multi_wrapper(self, *args, **kwargs):
403 if _has_cursor(args, kwargs):
404 cr, uid, ids, context, args, kwargs = split_args(args, kwargs)
405 with Scope(cr, uid, context):
406 value = method(self.browse(ids), *args, **kwargs)
407 return new_to_old(self, value)
408 else:
409 return method(self, *args, **kwargs)
410
411 return multi_wrapper
412
413
414def one(method):
415 """ Decorate a record-style method where `self` is expected to be a
416 singleton instance. The decorated method automatically loops on records,
417 and makes a list with the results. In case the method is decorated with
418 @returns, it concatenates the resulting instances. Such a method::
419
420 @api.one
421 def method(self, args):
422 return self.name
423
424 may be called in both record and traditional styles, like::
425
426 recs = model.browse(ids)
427
428 # the following calls are equivalent and return a list of names
429 recs.method(args)
430 model.method(cr, uid, recs.unbrowse(), args, context=context)
431 """
432 method._api = one
433 split_args = _cr_uid_ids_context_splitter(method)
434 new_to_old = _converter_to_old(method)
435 aggregate = _aggregator_one(method)
436
437 @wraps(method)
438 def one_wrapper(self, *args, **kwargs):
439 if _has_cursor(args, kwargs):
440 cr, uid, ids, context, args, kwargs = split_args(args, kwargs)
441 with Scope(cr, uid, context):
442 value = [method(rec, *args, **kwargs) for rec in self.browse(ids)]
443 return new_to_old(self, aggregate(self, value))
444 else:
445 value = [method(rec, *args, **kwargs) for rec in self]
446 return aggregate(self, value)
447
448 return one_wrapper
449
450
451def cr(method):
452 """ Decorate a traditional-style method that takes `cr` as a parameter.
453 Such a method may be called in both record and traditional styles, like::
454
455 obj.method(args) # record style
456 obj.method(cr, args) # traditional style
457 """
458 method._api = cr
459 get_scope = _scope_cr_getter(method)
460 old_to_new = _converter_to_new(method)
461
462 @wraps(method)
463 def cr_wrapper(self, *args, **kwargs):
464 if _has_cursor(args, kwargs):
465 with get_scope(args, kwargs):
466 return method(self, *args, **kwargs)
467 else:
468 value = method(self, scope.cr, *args, **kwargs)
469 return old_to_new(self, value)
470
471 return cr_wrapper
472
473
474def cr_context(method):
475 """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """
476 method._api = cr_context
477 get_scope = _scope_cr_context_getter(method)
478 old_to_new = _converter_to_new(method)
479
480 @wraps(method)
481 def cr_context_wrapper(self, *args, **kwargs):
482 if _has_cursor(args, kwargs):
483 with get_scope(args, kwargs):
484 return method(self, *args, **kwargs)
485 else:
486 cr, _, context = scope.args
487 kwargs['context'] = context
488 value = method(self, cr, *args, **kwargs)
489 return old_to_new(self, value)
490
491 return cr_context_wrapper
492
493
494def cr_uid(method):
495 """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
496 method._api = cr_uid
497 get_scope = _scope_cr_uid_getter(method)
498 old_to_new = _converter_to_new(method)
499
500 @wraps(method)
501 def cr_uid_wrapper(self, *args, **kwargs):
502 if _has_cursor(args, kwargs):
503 with get_scope(args, kwargs):
504 return method(self, *args, **kwargs)
505 else:
506 cr, uid, _ = scope.args
507 value = method(self, cr, uid, *args, **kwargs)
508 return old_to_new(self, value)
509
510 return cr_uid_wrapper
511
512
513def cr_uid_context(method):
514 """ Decorate a traditional-style method that takes `cr`, `uid`, `context` as
515 parameters. Such a method may be called in both record and traditional
516 styles, like::
517
518 obj.method(args)
519 obj.method(cr, uid, args, context=context)
520 """
521 method._api = cr_uid_context
522 get_scope = _scope_cr_uid_context_getter(method)
523 old_to_new = _converter_to_new(method)
524
525 @wraps(method)
526 def cr_uid_context_wrapper(self, *args, **kwargs):
527 if _has_cursor(args, kwargs):
528 with get_scope(args, kwargs):
529 return method(self, *args, **kwargs)
530 else:
531 cr, uid, context = scope.args
532 kwargs['context'] = context
533 value = method(self, cr, uid, *args, **kwargs)
534 return old_to_new(self, value)
535
536 return cr_uid_context_wrapper
537
538
539def cr_uid_id(method):
540 """ Decorate a traditional-style method that takes `cr`, `uid`, `id` as
541 parameters. Such a method may be called in both record and traditional
542 styles. In the record style, the method automatically loops on records.
543 """
544 method._api = cr_uid_id
545 get_scope = _scope_cr_uid_getter(method)
546 old_to_new = _converter_to_new(method)
547
548 @wraps(method)
549 def cr_uid_id_wrapper(self, *args, **kwargs):
550 if _has_cursor(args, kwargs):
551 with get_scope(args, kwargs):
552 return method(self, *args, **kwargs)
553 else:
554 cr, uid, _ = scope.args
555 value = [method(self, cr, uid, id, *args, **kwargs) for id in self.unbrowse()]
556 return old_to_new(self, value)
557
558 return cr_uid_id_wrapper
559
560
561def cr_uid_id_context(method):
562 """ Decorate a traditional-style method that takes `cr`, `uid`, `id`,
563 `context` as parameters. Such a method::
564
565 @api.cr_uid_id
566 def method(self, cr, uid, id, args, context=None):
567 ...
568
569 may be called in both record and traditional styles, like::
570
571 rec = model.browse(id)
572
573 # the following calls are equivalent
574 rec.method(args)
575 model.method(cr, uid, id, args, context=context)
576 """
577 method._api = cr_uid_id_context
578 get_scope = _scope_cr_uid_context_getter(method)
579 old_to_new = _converter_to_new(method)
580
581 @wraps(method)
582 def cr_uid_id_context_wrapper(self, *args, **kwargs):
583 if _has_cursor(args, kwargs):
584 with get_scope(args, kwargs):
585 return method(self, *args, **kwargs)
586 else:
587 cr, uid, context = scope.args
588 kwargs['context'] = context
589 value = [method(self, cr, uid, id, *args, **kwargs) for id in self.unbrowse()]
590 return old_to_new(self, value)
591
592 return cr_uid_id_context_wrapper
593
594
595def cr_uid_ids(method):
596 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids` as
597 parameters. Such a method may be called in both record and traditional
598 styles.
599 """
600 method._api = cr_uid_ids
601 get_scope = _scope_cr_uid_getter(method)
602 old_to_new = _converter_to_new(method)
603
604 @wraps(method)
605 def cr_uid_ids_wrapper(self, *args, **kwargs):
606 if _has_cursor(args, kwargs):
607 with get_scope(args, kwargs):
608 return method(self, *args, **kwargs)
609 else:
610 cr, uid, _ = scope.args
611 value = method(self, cr, uid, self.unbrowse(), *args, **kwargs)
612 return old_to_new(self, value)
613
614 return cr_uid_ids_wrapper
615
616
617def cr_uid_ids_context(method):
618 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids`,
619 `context` as parameters. Such a method::
620
621 @api.cr_uid_ids_context
622 def method(self, cr, uid, ids, args, context=None):
623 ...
624
625 may be called in both record and traditional styles, like::
626
627 recs = model.browse(ids)
628
629 # the following calls are equivalent
630 recs.method(args)
631 model.method(cr, uid, ids, args, context=context)
632
633 It is generally not necessary, see :func:`guess`.
634 """
635 method._api = cr_uid_ids_context
636 get_scope = _scope_cr_uid_context_getter(method)
637 old_to_new = _converter_to_new(method)
638
639 @wraps(method)
640 def cr_uid_ids_context_wrapper(self, *args, **kwargs):
641 if _has_cursor(args, kwargs):
642 with get_scope(args, kwargs):
643 return method(self, *args, **kwargs)
644 else:
645 cr, uid, context = scope.args
646 kwargs['context'] = context
647 value = method(self, cr, uid, self.unbrowse(), *args, **kwargs)
648 return old_to_new(self, value)
649
650 return cr_uid_ids_context_wrapper
651
652
653def _make_wrapper(method, old_api, new_api):
654 @wraps(method)
655 def wrapper(self, *args, **kwargs):
656 if _has_cursor(args, kwargs):
657 return old_api(self, *args, **kwargs)
658 else:
659 return new_api(self, *args, **kwargs)
660 return wrapper
661
662
663def old(method):
664 """ Decorate `method` so that it accepts the old-style api only. The
665 returned wrapper provides a decorator `new` for the corresponding
666 new-style api implementation; the result combines both implementations.
667 This is useful to provide explicitly both implementations of a method.::
668
669 @old
670 def stuff(self, cr, uid, context=None):
671 ...
672
673 @stuff.new
674 def stuff(self):
675 ...
676 """
677 wrapper = _make_wrapper(method, method, None)
678 wrapper.new = lambda new_api: _make_wrapper(method, method, new_api)
679 return wrapper
680
681
682def new(method):
683 """ Decorate `method` so that it accepts the new-style api only. The
684 returned wrapper provides a decorator `old` for the corresponding
685 old-style api implementation; the result combines both implementations.
686 This is useful to provide explicitly both implementations of a method.::
687
688 @new
689 def stuff(self):
690 ...
691
692 @stuff.old
693 def stuff(self, cr, uid, context=None):
694 ...
695 """
696 wrapper = _make_wrapper(method, None, method)
697 wrapper.old = lambda old_api: _make_wrapper(method, old_api, method)
698 return wrapper
699
700
701def noguess(method):
702 """ Decorate a method to prevent any effect from :func:`guess`. """
703 method._api = False
704 return method
705
706
707def guess(method):
708 """ Decorate `method` to make it callable in both traditional and record
709 styles. This decorator is applied automatically by the model's
710 metaclass, and has no effect on already-decorated methods.
711
712 The API style is determined by heuristics on the parameter names: ``cr``
713 or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
714 ``id`` or ``ids`` for a list of record ids, and ``context`` for the
715 context dictionary. If a traditional API is recognized, one of the
716 decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
717 :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
718 :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
719
720 Method calls are considered traditional style when their first parameter
721 is a database cursor.
722 """
723 # introspection on argument names to determine api style
724 names = tuple(getargspec(method).args) + (None,) * 4
725
726 if names[0] == 'self':
727 if names[1] in ('cr', 'cursor'):
728 if names[2] in ('uid', 'user'):
729 if names[3] == 'ids':
730 if 'context' in names:
731 return cr_uid_ids_context(method)
732 else:
733 return cr_uid_ids(method)
734 elif names[3] == 'id':
735 if 'context' in names:
736 return cr_uid_id_context(method)
737 else:
738 return cr_uid_id(method)
739 elif 'context' in names:
740 return cr_uid_context(method)
741 else:
742 return cr_uid(method)
743 elif 'context' in names:
744 return cr_context(method)
745 else:
746 return cr(method)
747
748 # no wrapping by default
749 return noguess(method)
750
751
752def expected(decorator, func):
753 """ Decorate `func` with `decorator` if `func` is not wrapped yet. """
754 return decorator(func) if not hasattr(func, '_orig') else func
755
756
757# keep those imports here in order to handle cyclic dependencies correctly
758from openerp import SUPERUSER_ID
759from openerp.osv.scope import Scope, proxy as scope
760from openerp.sql_db import Cursor
0761
=== modified file 'openerp/osv/expression.py'
--- openerp/osv/expression.py 2014-01-15 20:53:57 +0000
+++ openerp/osv/expression.py 2014-01-22 16:19:54 +0000
@@ -137,7 +137,7 @@
137137
138import openerp.modules138import openerp.modules
139from openerp.osv import fields139from openerp.osv import fields
140from openerp.osv.orm import MAGIC_COLUMNS140from openerp.osv.orm import MAGIC_COLUMNS, BaseModel
141import openerp.tools as tools141import openerp.tools as tools
142142
143143
@@ -512,7 +512,7 @@
512 in the condition (i.e. in many2one); this link is used to512 in the condition (i.e. in many2one); this link is used to
513 compute aliases513 compute aliases
514 """514 """
515 assert model, 'Invalid leaf creation without table'515 assert isinstance(model, BaseModel), 'Invalid leaf creation without table'
516 self.join_context = join_context or []516 self.join_context = join_context or []
517 self.leaf = leaf517 self.leaf = leaf
518 # normalize the leaf's operator518 # normalize the leaf's operator
@@ -673,20 +673,23 @@
673 - the leaf is added to the result673 - the leaf is added to the result
674674
675 Some internal var explanation:675 Some internal var explanation:
676 :var obj working_model: model object, model containing the field676 :var list path: left operand seen as a sequence of field names
677 ("foo.bar" -> ["foo", "bar"])
678 :var obj model: model object, model containing the field
677 (the name provided in the left operand)679 (the name provided in the left operand)
678 :var list field_path: left operand seen as a path (foo.bar -> [foo, bar])680 :var obj field: the field corresponding to `path[0]`
679 :var obj relational_model: relational model of a field (field._obj)681 :var obj column: the column corresponding to `path[0]`
680 ex: res_partner.bank_ids -> res.partner.bank682 :var obj comodel: relational model of field (field.comodel)
683 (res_partner.bank_ids -> res.partner.bank)
681 """684 """
682685
683 def to_ids(value, relational_model, context=None, limit=None):686 def to_ids(value, comodel, context=None, limit=None):
684 """ Normalize a single id or name, or a list of those, into a list of ids687 """ Normalize a single id or name, or a list of those, into a list of ids
685 :param {int,long,basestring,list,tuple} value:688 :param {int,long,basestring,list,tuple} value:
686 if int, long -> return [value]689 if int, long -> return [value]
687 if basestring, convert it into a list of basestrings, then690 if basestring, convert it into a list of basestrings, then
688 if list of basestring ->691 if list of basestring ->
689 perform a name_search on relational_model for each name692 perform a name_search on comodel for each name
690 return the list of related ids693 return the list of related ids
691 """694 """
692 names = []695 names = []
@@ -697,7 +700,7 @@
697 elif isinstance(value, (int, long)):700 elif isinstance(value, (int, long)):
698 return [value]701 return [value]
699 if names:702 if names:
700 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)]703 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)]
701 return list(set(name_get_list))704 return list(set(name_get_list))
702 return list(value)705 return list(value)
703706
@@ -747,7 +750,6 @@
747 leaf = pop()750 leaf = pop()
748751
749 # Get working variables752 # Get working variables
750 working_model = leaf.model
751 if leaf.is_operator():753 if leaf.is_operator():
752 left, operator, right = leaf.leaf, None, None754 left, operator, right = leaf.leaf, None, None
753 elif leaf.is_true_leaf() or leaf.is_false_leaf():755 elif leaf.is_true_leaf() or leaf.is_false_leaf():
@@ -755,12 +757,12 @@
755 left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2])757 left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2])
756 else:758 else:
757 left, operator, right = leaf.leaf759 left, operator, right = leaf.leaf
758 field_path = left.split('.', 1)760 path = left.split('.', 1)
759 field = working_model._columns.get(field_path[0])761
760 if field and field._obj:762 model = leaf.model
761 relational_model = working_model.pool[field._obj]763 field = model._fields.get(path[0])
762 else:764 column = model._columns.get(path[0])
763 relational_model = None765 comodel = getattr(field, 'comodel', None)
764766
765 # ----------------------------------------767 # ----------------------------------------
766 # SIMPLE CASE768 # SIMPLE CASE
@@ -783,22 +785,22 @@
783 # -> else: crash785 # -> else: crash
784 # ----------------------------------------786 # ----------------------------------------
785787
786 elif not field and field_path[0] in working_model._inherit_fields:788 elif not column and path[0] in model._inherit_fields:
787 # comments about inherits'd fields789 # comments about inherits'd fields
788 # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',790 # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',
789 # field_column_obj, origina_parent_model), ... }791 # field_column_obj, origina_parent_model), ... }
790 next_model = working_model.pool[working_model._inherit_fields[field_path[0]][0]]792 next_model = model.pool[model._inherit_fields[path[0]][0]]
791 leaf.add_join_context(next_model, working_model._inherits[next_model._name], 'id', working_model._inherits[next_model._name])793 leaf.add_join_context(next_model, model._inherits[next_model._name], 'id', model._inherits[next_model._name])
792 push(leaf)794 push(leaf)
793795
794 elif left == 'id' and operator == 'child_of':796 elif left == 'id' and operator == 'child_of':
795 ids2 = to_ids(right, working_model, context)797 ids2 = to_ids(right, model, context)
796 dom = child_of_domain(left, ids2, working_model)798 dom = child_of_domain(left, ids2, model)
797 for dom_leaf in reversed(dom):799 for dom_leaf in reversed(dom):
798 new_leaf = create_substitution_leaf(leaf, dom_leaf, working_model)800 new_leaf = create_substitution_leaf(leaf, dom_leaf, model)
799 push(new_leaf)801 push(new_leaf)
800802
801 elif not field and field_path[0] in MAGIC_COLUMNS:803 elif not column and path[0] in MAGIC_COLUMNS:
802 push_result(leaf)804 push_result(leaf)
803805
804 elif not field:806 elif not field:
@@ -807,70 +809,88 @@
807 # ----------------------------------------809 # ----------------------------------------
808 # PATH SPOTTED810 # PATH SPOTTED
809 # -> many2one or one2many with _auto_join:811 # -> many2one or one2many with _auto_join:
810 # - add a join, then jump into linked field: field.remaining on812 # - add a join, then jump into linked column: column.remaining on
811 # src_table is replaced by remaining on dst_table, and set for re-evaluation813 # src_table is replaced by remaining on dst_table, and set for re-evaluation
812 # - if a domain is defined on the field, add it into evaluation814 # - if a domain is defined on the column, add it into evaluation
813 # on the relational table815 # on the relational table
814 # -> many2one, many2many, one2many: replace by an equivalent computed816 # -> many2one, many2many, one2many: replace by an equivalent computed
815 # domain, given by recursively searching on the remaining of the path817 # domain, given by recursively searching on the remaining of the path
816 # -> note: hack about fields.property should not be necessary anymore818 # -> note: hack about columns.property should not be necessary anymore
817 # as after transforming the field, it will go through this loop once again819 # as after transforming the column, it will go through this loop once again
818 # ----------------------------------------820 # ----------------------------------------
819821
820 elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join:822 elif len(path) > 1 and column._type == 'many2one' and column._auto_join:
821 # res_partner.state_id = res_partner__state_id.id823 # res_partner.state_id = res_partner__state_id.id
822 leaf.add_join_context(relational_model, field_path[0], 'id', field_path[0])824 leaf.add_join_context(comodel, path[0], 'id', path[0])
823 push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))825 push(create_substitution_leaf(leaf, (path[1], operator, right), comodel))
824826
825 elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join:827 elif len(path) > 1 and column._type == 'one2many' and column._auto_join:
826 # res_partner.id = res_partner__bank_ids.partner_id828 # res_partner.id = res_partner__bank_ids.partner_id
827 leaf.add_join_context(relational_model, 'id', field._fields_id, field_path[0])829 leaf.add_join_context(comodel, 'id', column._fields_id, path[0])
828 domain = field._domain(working_model) if callable(field._domain) else field._domain830 domain = column._domain(model) if callable(column._domain) else column._domain
829 push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))831 push(create_substitution_leaf(leaf, (path[1], operator, right), comodel))
830 if domain:832 if domain:
831 domain = normalize_domain(domain)833 domain = normalize_domain(domain)
832 for elem in reversed(domain):834 for elem in reversed(domain):
833 push(create_substitution_leaf(leaf, elem, relational_model))835 push(create_substitution_leaf(leaf, elem, comodel))
834 push(create_substitution_leaf(leaf, AND_OPERATOR, relational_model))836 push(create_substitution_leaf(leaf, AND_OPERATOR, comodel))
835837
836 elif len(field_path) > 1 and field._auto_join:838 elif len(path) > 1 and column._auto_join:
837 raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % left)839 raise NotImplementedError('_auto_join attribute not supported on many2many column %s' % left)
838840
839 elif len(field_path) > 1 and field._type == 'many2one':841 elif len(path) > 1 and column._type == 'many2one':
840 right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)842 right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=context)
841 leaf.leaf = (field_path[0], 'in', right_ids)843 leaf.leaf = (path[0], 'in', right_ids)
842 push(leaf)844 push(leaf)
843845
844 # Making search easier when there is a left operand as field.o2m or field.m2m846 # Making search easier when there is a left operand as column.o2m or column.m2m
845 elif len(field_path) > 1 and field._type in ['many2many', 'one2many']:847 elif len(path) > 1 and column._type in ['many2many', 'one2many']:
846 right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)848 right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=context)
847 table_ids = working_model.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False))849 table_ids = model.search(cr, uid, [(path[0], 'in', right_ids)], context=dict(context, active_test=False))
848 leaf.leaf = ('id', 'in', table_ids)850 leaf.leaf = ('id', 'in', table_ids)
849 push(leaf)851 push(leaf)
850852
853 elif not field.store:
854 # Non-stored field should provide an implementation of search.
855 if not field.search:
856 # field does not support search!
857 _logger.error("Non-stored field %s cannot be searched.", field)
858 if _logger.isEnabledFor(logging.DEBUG):
859 _logger.debug(''.join(traceback.format_stack()))
860 # Ignore it: generate a dummy leaf.
861 domain = []
862 else:
863 # Let the field generate a domain.
864 domain = field.determine_domain(operator, right)
865
866 if not domain:
867 leaf.leaf = TRUE_LEAF
868 push(leaf)
869 else:
870 for elem in reversed(domain):
871 push(create_substitution_leaf(leaf, elem, model))
872
851 # -------------------------------------------------873 # -------------------------------------------------
852 # FUNCTION FIELD874 # FUNCTION FIELD
853 # -> not stored: error if no _fnct_search, otherwise handle the result domain875 # -> not stored: error if no _fnct_search, otherwise handle the result domain
854 # -> stored: management done in the remaining of parsing876 # -> stored: management done in the remaining of parsing
855 # -------------------------------------------------877 # -------------------------------------------------
856878
857 elif isinstance(field, fields.function) and not field.store and not field._fnct_search:879 elif isinstance(column, fields.function) and not column.store:
858 # this is a function field that is not stored880 # this is a function field that is not stored
859 # the function field doesn't provide a search function and doesn't store881 if not column._fnct_search:
860 # values in the database, so we must ignore it : we generate a dummy leaf882 _logger.error(
861 leaf.leaf = TRUE_LEAF883 "Field '%s' (%s) can not be searched: "
862 _logger.error(884 "non-stored function field without fnct_search",
863 "The field '%s' (%s) can not be searched: non-stored "885 column.string, left)
864 "function field without fnct_search",886 # avoid compiling stack trace if not needed
865 field.string, left)887 if _logger.isEnabledFor(logging.DEBUG):
866 # avoid compiling stack trace if not needed888 _logger.debug(''.join(traceback.format_stack()))
867 if _logger.isEnabledFor(logging.DEBUG):889 # ignore it: generate a dummy leaf
868 _logger.debug(''.join(traceback.format_stack()))890 fct_domain = []
869 push(leaf)891 else:
892 fct_domain = column.search(cr, uid, model, left, [leaf.leaf], context=context)
870893
871 elif isinstance(field, fields.function) and not field.store:
872 # this is a function field that is not stored
873 fct_domain = field.search(cr, uid, working_model, left, [leaf.leaf], context=context)
874 if not fct_domain:894 if not fct_domain:
875 leaf.leaf = TRUE_LEAF895 leaf.leaf = TRUE_LEAF
876 push(leaf)896 push(leaf)
@@ -878,30 +898,30 @@
878 # we assume that the expression is valid898 # we assume that the expression is valid
879 # we create a dummy leaf for forcing the parsing of the resulting expression899 # we create a dummy leaf for forcing the parsing of the resulting expression
880 for domain_element in reversed(fct_domain):900 for domain_element in reversed(fct_domain):
881 push(create_substitution_leaf(leaf, domain_element, working_model))901 push(create_substitution_leaf(leaf, domain_element, model))
882 # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_model))902 # self.push(create_substitution_leaf(leaf, TRUE_LEAF, model))
883 # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_model))903 # self.push(create_substitution_leaf(leaf, AND_OPERATOR, model))
884904
885 # -------------------------------------------------905 # -------------------------------------------------
886 # RELATIONAL FIELDS906 # RELATIONAL FIELDS
887 # -------------------------------------------------907 # -------------------------------------------------
888908
889 # Applying recursivity on field(one2many)909 # Applying recursivity on field(one2many)
890 elif field._type == 'one2many' and operator == 'child_of':910 elif column._type == 'one2many' and operator == 'child_of':
891 ids2 = to_ids(right, relational_model, context)911 ids2 = to_ids(right, comodel, context)
892 if field._obj != working_model._name:912 if column._obj != model._name:
893 dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)913 dom = child_of_domain(left, ids2, comodel, prefix=column._obj)
894 else:914 else:
895 dom = child_of_domain('id', ids2, working_model, parent=left)915 dom = child_of_domain('id', ids2, model, parent=left)
896 for dom_leaf in reversed(dom):916 for dom_leaf in reversed(dom):
897 push(create_substitution_leaf(leaf, dom_leaf, working_model))917 push(create_substitution_leaf(leaf, dom_leaf, model))
898918
899 elif field._type == 'one2many':919 elif column._type == 'one2many':
900 call_null = True920 call_null = True
901921
902 if right is not False:922 if right is not False:
903 if isinstance(right, basestring):923 if isinstance(right, basestring):
904 ids2 = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context, limit=None)]924 ids2 = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context, limit=None)]
905 if ids2:925 if ids2:
906 operator = 'in'926 operator = 'in'
907 else:927 else:
@@ -913,36 +933,36 @@
913 if operator in ['like', 'ilike', 'in', '=']:933 if operator in ['like', 'ilike', 'in', '=']:
914 #no result found with given search criteria934 #no result found with given search criteria
915 call_null = False935 call_null = False
916 push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))936 push(create_substitution_leaf(leaf, FALSE_LEAF, model))
917 else:937 else:
918 ids2 = select_from_where(cr, field._fields_id, relational_model._table, 'id', ids2, operator)938 ids2 = select_from_where(cr, column._fields_id, comodel._table, 'id', ids2, operator)
919 if ids2:939 if ids2:
920 call_null = False940 call_null = False
921 o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'941 o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
922 push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_model))942 push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), model))
923943
924 if call_null:944 if call_null:
925 o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'945 o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
926 push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_model._table)), working_model))946 push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model))
927947
928 elif field._type == 'many2many':948 elif column._type == 'many2many':
929 rel_table, rel_id1, rel_id2 = field._sql_names(working_model)949 rel_table, rel_id1, rel_id2 = column._sql_names(model)
930 #FIXME950 #FIXME
931 if operator == 'child_of':951 if operator == 'child_of':
932 def _rec_convert(ids):952 def _rec_convert(ids):
933 if relational_model == working_model:953 if comodel == model:
934 return ids954 return ids
935 return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)955 return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)
936956
937 ids2 = to_ids(right, relational_model, context)957 ids2 = to_ids(right, comodel, context)
938 dom = child_of_domain('id', ids2, relational_model)958 dom = child_of_domain('id', ids2, comodel)
939 ids2 = relational_model.search(cr, uid, dom, context=context)959 ids2 = comodel.search(cr, uid, dom, context=context)
940 push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_model))960 push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model))
941 else:961 else:
942 call_null_m2m = True962 call_null_m2m = True
943 if right is not False:963 if right is not False:
944 if isinstance(right, basestring):964 if isinstance(right, basestring):
945 res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context)]965 res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, context=context)]
946 if res_ids:966 if res_ids:
947 operator = 'in'967 operator = 'in'
948 else:968 else:
@@ -954,29 +974,29 @@
954 if operator in ['like', 'ilike', 'in', '=']:974 if operator in ['like', 'ilike', 'in', '=']:
955 #no result found with given search criteria975 #no result found with given search criteria
956 call_null_m2m = False976 call_null_m2m = False
957 push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))977 push(create_substitution_leaf(leaf, FALSE_LEAF, model))
958 else:978 else:
959 operator = 'in' # operator changed because ids are directly related to main object979 operator = 'in' # operator changed because ids are directly related to main object
960 else:980 else:
961 call_null_m2m = False981 call_null_m2m = False
962 m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'982 m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
963 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))983 push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model))
964984
965 if call_null_m2m:985 if call_null_m2m:
966 m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'986 m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
967 push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_model))987 push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), model))
968988
969 elif field._type == 'many2one':989 elif column._type == 'many2one':
970 if operator == 'child_of':990 if operator == 'child_of':
971 ids2 = to_ids(right, relational_model, context)991 ids2 = to_ids(right, comodel, context)
972 if field._obj != working_model._name:992 if column._obj != model._name:
973 dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)993 dom = child_of_domain(left, ids2, comodel, prefix=column._obj)
974 else:994 else:
975 dom = child_of_domain('id', ids2, working_model, parent=left)995 dom = child_of_domain('id', ids2, model, parent=left)
976 for dom_leaf in reversed(dom):996 for dom_leaf in reversed(dom):
977 push(create_substitution_leaf(leaf, dom_leaf, working_model))997 push(create_substitution_leaf(leaf, dom_leaf, model))
978 else:998 else:
979 def _get_expression(relational_model, cr, uid, left, right, operator, context=None):999 def _get_expression(comodel, cr, uid, left, right, operator, context=None):
980 if context is None:1000 if context is None:
981 context = {}1001 context = {}
982 c = context.copy()1002 c = context.copy()
@@ -991,14 +1011,14 @@
991 operator = dict_op[operator]1011 operator = dict_op[operator]
992 elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2'])1012 elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2'])
993 operator = dict_op[operator]1013 operator = dict_op[operator]
994 res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, limit=None, context=c)]1014 res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], operator, limit=None, context=c)]
995 if operator in NEGATIVE_TERM_OPERATORS:1015 if operator in NEGATIVE_TERM_OPERATORS:
996 res_ids.append(False) # TODO this should not be appended if False was in 'right'1016 res_ids.append(False) # TODO this should not be appended if False was in 'right'
997 return left, 'in', res_ids1017 return left, 'in', res_ids
998 # resolve string-based m2o criterion into IDs1018 # resolve string-based m2o criterion into IDs
999 if isinstance(right, basestring) or \1019 if isinstance(right, basestring) or \
1000 right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right):1020 right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right):
1001 push(create_substitution_leaf(leaf, _get_expression(relational_model, cr, uid, left, right, operator, context=context), working_model))1021 push(create_substitution_leaf(leaf, _get_expression(comodel, cr, uid, left, right, operator, context=context), model))
1002 else:1022 else:
1003 # right == [] or right == False and all other cases are handled by __leaf_to_sql()1023 # right == [] or right == False and all other cases are handled by __leaf_to_sql()
1004 push_result(leaf)1024 push_result(leaf)
@@ -1006,19 +1026,19 @@
1006 # -------------------------------------------------1026 # -------------------------------------------------
1007 # OTHER FIELDS1027 # OTHER FIELDS
1008 # -> datetime fields: manage time part of the datetime1028 # -> datetime fields: manage time part of the datetime
1009 # field when it is not there1029 # column when it is not there
1010 # -> manage translatable fields1030 # -> manage translatable fields
1011 # -------------------------------------------------1031 # -------------------------------------------------
10121032
1013 else:1033 else:
1014 if field._type == 'datetime' and right and len(right) == 10:1034 if column._type == 'datetime' and right and len(right) == 10:
1015 if operator in ('>', '>=', '='):1035 if operator in ('>', '>=', '='):
1016 right += ' 00:00:00'1036 right += ' 00:00:00'
1017 elif operator in ('<', '<='):1037 elif operator in ('<', '<='):
1018 right += ' 23:59:59'1038 right += ' 23:59:59'
1019 push(create_substitution_leaf(leaf, (left, operator, right), working_model))1039 push(create_substitution_leaf(leaf, (left, operator, right), model))
10201040
1021 elif field.translate:1041 elif column.translate:
1022 need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')1042 need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
1023 sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)1043 sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
1024 if need_wildcard:1044 if need_wildcard:
@@ -1042,22 +1062,22 @@
1042 subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \1062 subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \
1043 ') UNION (' \1063 ') UNION (' \
1044 ' SELECT id' \1064 ' SELECT id' \
1045 ' FROM "' + working_model._table + '"' \1065 ' FROM "' + model._table + '"' \
1046 ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))"1066 ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))"
1047 else:1067 else:
1048 subselect += ' AND value ' + sql_operator + instr + \1068 subselect += ' AND value ' + sql_operator + instr + \
1049 ') UNION (' \1069 ') UNION (' \
1050 ' SELECT id' \1070 ' SELECT id' \
1051 ' FROM "' + working_model._table + '"' \1071 ' FROM "' + model._table + '"' \
1052 ' WHERE "' + left + '" ' + sql_operator + instr + ")"1072 ' WHERE "' + left + '" ' + sql_operator + instr + ")"
10531073
1054 params = [working_model._name + ',' + left,1074 params = [model._name + ',' + left,
1055 context.get('lang', False) or 'en_US',1075 context.get('lang', False) or 'en_US',
1056 'model',1076 'model',
1057 right,1077 right,
1058 right,1078 right,
1059 ]1079 ]
1060 push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), working_model))1080 push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), model))
10611081
1062 else:1082 else:
1063 push_result(leaf)1083 push_result(leaf)
10641084
=== modified file 'openerp/osv/fields.py'
--- openerp/osv/fields.py 2014-01-15 20:53:57 +0000
+++ openerp/osv/fields.py 2014-01-22 16:19:54 +0000
@@ -93,6 +93,9 @@
93 """93 """
94 if domain is None:94 if domain is None:
95 domain = []95 domain = []
96 elif callable(domain):
97 from openerp import api
98 domain = api.expected(api.cr_uid_context, domain)
96 if context is None:99 if context is None:
97 context = {}100 context = {}
98 self.states = states or {}101 self.states = states or {}
@@ -117,7 +120,35 @@
117 self.deprecated = False # Optional deprecation warning120 self.deprecated = False # Optional deprecation warning
118 for a in args:121 for a in args:
119 setattr(self, a, args[a])122 setattr(self, a, args[a])
120 123
124 # prefetch only if self._classic_write, not self.groups, and not
125 # self.deprecated
126 if not self._classic_write or self.groups or self.deprecated:
127 self._prefetch = False
128
129 def to_field(self):
130 """ convert column `self` to a new-style field """
131 from openerp.osv.fields2 import Field
132 return Field.by_type[self._type](**self.to_field_args())
133
134 def to_field_args(self):
135 """ return a dictionary with all the arguments to pass to the field """
136 items = [
137 ('interface_for', self), # field interfaces self
138 ('string', self.string),
139 ('help', self.help),
140 ('readonly', self.readonly),
141 ('required', self.required),
142 ('states', self.states),
143 ('groups', self.groups),
144 ('size', self.size),
145 ('ondelete', self.ondelete),
146 ('translate', self.translate),
147 ('domain', self._domain),
148 ('context', self._context),
149 ]
150 return dict(item for item in items if items[1])
151
121 def restart(self):152 def restart(self):
122 pass153 pass
123154
@@ -182,8 +213,16 @@
182 _classic_read = False # post-process to handle missing target213 _classic_read = False # post-process to handle missing target
183214
184 def __init__(self, string, selection, size=None, **args):215 def __init__(self, string, selection, size=None, **args):
216 if callable(selection):
217 from openerp import api
218 selection = api.expected(api.cr_uid_context, selection)
185 _column.__init__(self, string=string, size=size, selection=selection, **args)219 _column.__init__(self, string=string, size=size, selection=selection, **args)
186220
221 def to_field_args(self):
222 args = super(reference, self).to_field_args()
223 args['selection'] = self.selection
224 return args
225
187 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):226 def get(self, cr, obj, ids, name, uid=None, context=None, values=None):
188 result = {}227 result = {}
189 # copy initial values fetched previously.228 # copy initial values fetched previously.
@@ -231,7 +270,6 @@
231 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)270 self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x)
232 self._symbol_set = (self._symbol_c, self._symbol_f)271 self._symbol_set = (self._symbol_c, self._symbol_f)
233272
234
235class text(_column):273class text(_column):
236 _type = 'text'274 _type = 'text'
237275
@@ -260,6 +298,11 @@
260 # synopsis: digits_compute(cr) -> (precision, scale)298 # synopsis: digits_compute(cr) -> (precision, scale)
261 self.digits_compute = digits_compute299 self.digits_compute = digits_compute
262300
301 def to_field_args(self):
302 args = super(float, self).to_field_args()
303 args['digits'] = self.digits_compute or self.digits
304 return args
305
263 def digits_change(self, cr):306 def digits_change(self, cr):
264 if self.digits_compute:307 if self.digits_compute:
265 self.digits = self.digits_compute(cr)308 self.digits = self.digits_compute(cr)
@@ -321,7 +364,8 @@
321 if context and context.get('tz'):364 if context and context.get('tz'):
322 tz_name = context['tz'] 365 tz_name = context['tz']
323 else:366 else:
324 tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']367 user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid)
368 tz_name = user.tz
325 if tz_name:369 if tz_name:
326 try:370 try:
327 utc = pytz.timezone('UTC')371 utc = pytz.timezone('UTC')
@@ -385,7 +429,8 @@
385 tz_name = context['tz'] 429 tz_name = context['tz']
386 else:430 else:
387 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)431 registry = openerp.modules.registry.RegistryManager.get(cr.dbname)
388 tz_name = registry.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']432 user = registry['res.users'].browse(cr, SUPERUSER_ID, uid)
433 tz_name = user.tz
389 if tz_name:434 if tz_name:
390 try:435 try:
391 utc = pytz.timezone('UTC')436 utc = pytz.timezone('UTC')
@@ -448,9 +493,17 @@
448 _type = 'selection'493 _type = 'selection'
449494
450 def __init__(self, selection, string='unknown', **args):495 def __init__(self, selection, string='unknown', **args):
496 if callable(selection):
497 from openerp import api
498 selection = api.expected(api.cr_uid_context, selection)
451 _column.__init__(self, string=string, **args)499 _column.__init__(self, string=string, **args)
452 self.selection = selection500 self.selection = selection
453501
502 def to_field_args(self):
503 args = super(selection, self).to_field_args()
504 args['selection'] = self.selection
505 return args
506
454# ---------------------------------------------------------507# ---------------------------------------------------------
455# Relationals fields508# Relationals fields
456# ---------------------------------------------------------509# ---------------------------------------------------------
@@ -477,6 +530,12 @@
477 self._obj = obj530 self._obj = obj
478 self._auto_join = auto_join531 self._auto_join = auto_join
479532
533 def to_field_args(self):
534 args = super(many2one, self).to_field_args()
535 args['comodel_name'] = self._obj
536 args['auto_join'] = self._auto_join
537 return args
538
480 def get(self, cr, obj, ids, name, user=None, context=None, values=None):539 def get(self, cr, obj, ids, name, user=None, context=None, values=None):
481 if context is None:540 if context is None:
482 context = {}541 context = {}
@@ -530,7 +589,6 @@
530 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):589 def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
531 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)590 return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context)
532591
533
534 @classmethod592 @classmethod
535 def _as_display_name(cls, field, cr, uid, obj, value, context=None):593 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
536 return value[1] if isinstance(value, tuple) else tools.ustr(value) 594 return value[1] if isinstance(value, tuple) else tools.ustr(value)
@@ -551,26 +609,34 @@
551 #one2many can't be used as condition for defaults609 #one2many can't be used as condition for defaults
552 assert(self.change_default != True)610 assert(self.change_default != True)
553611
612 def to_field_args(self):
613 args = super(one2many, self).to_field_args()
614 args['comodel_name'] = self._obj
615 args['inverse_name'] = self._fields_id
616 args['auto_join'] = self._auto_join
617 args['limit'] = self._limit
618 return args
619
554 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):620 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
555 if context is None:
556 context = {}
557 if self._context:621 if self._context:
558 context = context.copy()622 context = dict(context or {})
559 context.update(self._context)623 context.update(self._context)
560 if values is None:624
561 values = {}625 with Scope(cr, user, context):
562626 res = dict((id, []) for id in ids)
563 res = {}627
564 for id in ids:628 comodel = obj.pool[self._obj]
565 res[id] = []629 inverse = self._fields_id
566630 domain = self._domain
567 domain = self._domain(obj) if callable(self._domain) else self._domain631 if callable(domain):
568 model = obj.pool[self._obj]632 domain = domain(obj)
569 ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)633 domain = domain + [(inverse, 'in', ids)]
570 for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):634
571 if r[self._fields_id] in res:635 for record in comodel.search(domain, limit=self._limit):
572 res[r[self._fields_id]].append(r['id'])636 assert int(record[inverse]) in res
573 return res637 res[int(record[inverse])].append(record.id)
638
639 return res
574640
575 def set(self, cr, obj, id, field, values, user=None, context=None):641 def set(self, cr, obj, id, field, values, user=None, context=None):
576 result = []642 result = []
@@ -632,7 +698,6 @@
632 domain = self._domain(obj) if callable(self._domain) else self._domain698 domain = self._domain(obj) if callable(self._domain) else self._domain
633 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)699 return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit)
634700
635
636 @classmethod701 @classmethod
637 def _as_display_name(cls, field, cr, uid, obj, value, context=None):702 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
638 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)') 703 raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)')
@@ -691,6 +756,15 @@
691 self._id2 = id2756 self._id2 = id2
692 self._limit = limit757 self._limit = limit
693758
759 def to_field_args(self):
760 args = super(many2many, self).to_field_args()
761 args['comodel_name'] = self._obj
762 args['relation'] = self._rel
763 args['column1'] = self._id1
764 args['column2'] = self._id2
765 args['limit'] = self._limit
766 return args
767
694 def _sql_names(self, source_model):768 def _sql_names(self, source_model):
695 """Return the SQL names defining the structure of the m2m relationship table769 """Return the SQL names defining the structure of the m2m relationship table
696770
@@ -1084,6 +1158,9 @@
10841158
1085 self.digits = args.get('digits', (16,2))1159 self.digits = args.get('digits', (16,2))
1086 self.digits_compute = args.get('digits_compute', None)1160 self.digits_compute = args.get('digits_compute', None)
1161 if callable(args.get('selection')):
1162 from openerp import api
1163 self.selection = api.expected(api.cr_uid_context, args['selection'])
10871164
1088 self._fnct_inv_arg = fnct_inv_arg1165 self._fnct_inv_arg = fnct_inv_arg
1089 if not fnct_inv:1166 if not fnct_inv:
@@ -1105,25 +1182,26 @@
1105 else:1182 else:
1106 self._prefetch = True1183 self._prefetch = True
11071184
1108 if type == 'float':
1109 self._symbol_c = float._symbol_c
1110 self._symbol_f = float._symbol_f
1111 self._symbol_set = float._symbol_set
1112
1113 if type == 'boolean':
1114 self._symbol_c = boolean._symbol_c
1115 self._symbol_f = boolean._symbol_f
1116 self._symbol_set = boolean._symbol_set
1117
1118 if type == 'integer':
1119 self._symbol_c = integer._symbol_c
1120 self._symbol_f = integer._symbol_f
1121 self._symbol_set = integer._symbol_set
1122
1123 if type == 'char':1185 if type == 'char':
1124 self._symbol_c = char._symbol_c1186 self._symbol_c = char._symbol_c
1125 self._symbol_f = lambda x: _symbol_set_char(self, x)1187 self._symbol_f = lambda x: _symbol_set_char(self, x)
1126 self._symbol_set = (self._symbol_c, self._symbol_f)1188 self._symbol_set = (self._symbol_c, self._symbol_f)
1189 else:
1190 type_class = globals().get(type)
1191 if type_class is not None:
1192 self._symbol_c = type_class._symbol_c
1193 self._symbol_f = type_class._symbol_f
1194 self._symbol_set = type_class._symbol_set
1195
1196 def to_field_args(self):
1197 args = super(function, self).to_field_args()
1198 if self._type in ('float',):
1199 args['digits'] = self.digits_compute or self.digits
1200 elif self._type in ('selection', 'reference'):
1201 args['selection'] = self.selection
1202 elif self._type in ('many2one', 'one2many', 'many2many'):
1203 args['comodel_name'] = self._obj
1204 return args
11271205
1128 def digits_change(self, cr):1206 def digits_change(self, cr):
1129 if self._type == 'float':1207 if self._type == 'float':
@@ -1139,7 +1217,8 @@
1139 if not self._fnct_search:1217 if not self._fnct_search:
1140 #CHECKME: should raise an exception1218 #CHECKME: should raise an exception
1141 return []1219 return []
1142 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)1220 with Scope(cr, uid, context):
1221 return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
11431222
1144 def postprocess(self, cr, uid, obj, field, value=None, context=None):1223 def postprocess(self, cr, uid, obj, field, value=None, context=None):
1145 if context is None:1224 if context is None:
@@ -1170,7 +1249,8 @@
1170 return result1249 return result
11711250
1172 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):1251 def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
1173 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)1252 with Scope(cr, uid, context):
1253 result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
1174 for id in ids:1254 for id in ids:
1175 if self._multi and id in result:1255 if self._multi and id in result:
1176 for field, value in result[id].iteritems():1256 for field, value in result[id].iteritems():
@@ -1184,7 +1264,8 @@
1184 if not context:1264 if not context:
1185 context = {}1265 context = {}
1186 if self._fnct_inv:1266 if self._fnct_inv:
1187 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)1267 with Scope(cr, user, context):
1268 self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
11881269
1189 @classmethod1270 @classmethod
1190 def _as_display_name(cls, field, cr, uid, obj, value, context=None):1271 def _as_display_name(cls, field, cr, uid, obj, value, context=None):
@@ -1213,45 +1294,36 @@
1213 field = '.'.join(self._arg)1294 field = '.'.join(self._arg)
1214 return map(lambda x: (field, x[1], x[2]), domain)1295 return map(lambda x: (field, x[1], x[2]), domain)
12151296
1216 def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):1297 def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
1217 if isinstance(ids, (int, long)):1298 if isinstance(ids, (int, long)):
1218 ids = [ids]1299 ids = [ids]
1219 for record in obj.browse(cr, uid, ids, context=context):1300 for instance in obj.browse(cr, uid, ids, context=context):
1220 # traverse all fields except the last one1301 # traverse all fields except the last one
1221 for field in self.arg[:-1]:1302 for field in self.arg[:-1]:
1222 record = record[field] or False1303 instance = instance[field]
1223 if not record:1304 if instance:
1224 break1305 # write on the last field of the first record
1225 elif isinstance(record, list):1306 instance[0].write({self.arg[-1]: values})
1226 # record is the result of a one2many or many2many field
1227 record = record[0]
1228 if record:
1229 # write on the last field
1230 record.write({self.arg[-1]: values})
12311307
1232 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):1308 def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
1233 res = {}1309 res = {}
1234 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):1310 for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
1235 value = record1311 value = record
1236 for field in self.arg:1312 for field in self.arg:
1237 if isinstance(value, list):1313 value = value[field]
1238 value = value[0]
1239 value = value[field] or False
1240 if not value:
1241 break
1242 res[record.id] = value1314 res[record.id] = value
12431315
1244 if self._type == 'many2one':1316 if self._type == 'many2one':
1245 # res[id] is a browse_record or False; convert it to (id, name) or False.1317 # res[id] is a recordset; convert it to (id, name) or False.
1246 # Perform name_get as root, as seeing the name of a related object depends on1318 # Perform name_get as root, as seeing the name of a related object depends on
1247 # access right of source document, not target, so user may not have access.1319 # access right of source document, not target, so user may not have access.
1248 value_ids = list(set(value.id for value in res.itervalues() if value))1320 value_ids = list(set(value.id for value in res.itervalues() if value))
1249 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))1321 value_name = dict(obj.pool[self._obj].name_get(cr, SUPERUSER_ID, value_ids, context=context))
1250 res = dict((id, value and (value.id, value_name[value.id])) for id, value in res.iteritems())1322 res = dict((id, bool(value) and (value.id, value_name[value.id])) for id, value in res.iteritems())
12511323
1252 elif self._type in ('one2many', 'many2many'):1324 elif self._type in ('one2many', 'many2many'):
1253 # res[id] is a list of browse_record or False; convert it to a list of ids1325 # res[id] is a recordset; convert it to a list of ids
1254 res = dict((id, value and map(int, value) or []) for id, value in res.iteritems())1326 res = dict((id, value.unbrowse()) for id, value in res.iteritems())
12551327
1256 return res1328 return res
12571329
@@ -1445,7 +1517,7 @@
1445 default_val = self._get_default(obj, cr, uid, prop_name, context)1517 default_val = self._get_default(obj, cr, uid, prop_name, context)
14461518
1447 property_create = False1519 property_create = False
1448 if isinstance(default_val, openerp.osv.orm.browse_record):1520 if isinstance(default_val, openerp.osv.orm.BaseModel):
1449 if default_val.id != id_val:1521 if default_val.id != id_val:
1450 property_create = True1522 property_create = True
1451 elif default_val != id_val:1523 elif default_val != id_val:
@@ -1532,52 +1604,6 @@
1532 self.field_id = {}1604 self.field_id = {}
15331605
15341606
1535def field_to_dict(model, cr, user, field, context=None):
1536 """ Return a dictionary representation of a field.
1537
1538 The string, help, and selection attributes (if any) are untranslated. This
1539 representation is the one returned by fields_get() (fields_get() will do
1540 the translation).
1541
1542 """
1543
1544 res = {'type': field._type}
1545 # some attributes for m2m/function field are added as debug info only
1546 if isinstance(field, function):
1547 res['function'] = field._fnct and field._fnct.func_name or False
1548 res['store'] = field.store
1549 if isinstance(field.store, dict):
1550 res['store'] = str(field.store)
1551 res['fnct_search'] = field._fnct_search and field._fnct_search.func_name or False
1552 res['fnct_inv'] = field._fnct_inv and field._fnct_inv.func_name or False
1553 res['fnct_inv_arg'] = field._fnct_inv_arg or False
1554 if isinstance(field, many2many):
1555 (table, col1, col2) = field._sql_names(model)
1556 res['m2m_join_columns'] = [col1, col2]
1557 res['m2m_join_table'] = table
1558 for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
1559 'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
1560 'deprecated', 'digits', 'invisible', 'filters'):
1561 if getattr(field, arg, None):
1562 res[arg] = getattr(field, arg)
1563
1564 if hasattr(field, 'selection'):
1565 if isinstance(field.selection, (tuple, list)):
1566 res['selection'] = field.selection
1567 else:
1568 # call the 'dynamic selection' function
1569 res['selection'] = field.selection(model, cr, user, context)
1570 if res['type'] in ('one2many', 'many2many', 'many2one'):
1571 res['relation'] = field._obj
1572 res['domain'] = field._domain(model) if callable(field._domain) else field._domain
1573 res['context'] = field._context
1574
1575 if isinstance(field, one2many):
1576 res['relation_field'] = field._fields_id
1577
1578 return res
1579
1580
1581class column_info(object):1607class column_info(object):
1582 """ Struct containing details about an osv column, either one local to1608 """ Struct containing details about an osv column, either one local to
1583 its model, or one inherited via _inherits.1609 its model, or one inherited via _inherits.
@@ -1618,5 +1644,7 @@
1618 self.__class__.__name__, self.name, self.column,1644 self.__class__.__name__, self.name, self.column,
1619 self.parent_model, self.parent_column, self.original_parent)1645 self.parent_model, self.parent_column, self.original_parent)
16201646
1647
1648from openerp.osv.scope import Scope
1649
1621# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:1650# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1622
16231651
=== added file 'openerp/osv/fields2.py'
--- openerp/osv/fields2.py 1970-01-01 00:00:00 +0000
+++ openerp/osv/fields2.py 2014-01-22 16:19:54 +0000
@@ -0,0 +1,995 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22""" High-level objects for fields. """
23
24from copy import copy
25from datetime import date, datetime
26from functools import partial
27from operator import attrgetter
28import logging
29
30from openerp.tools import float_round, ustr, html_sanitize, lazy_property
31from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
32from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
33
34DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
35DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
36
37_logger = logging.getLogger(__name__)
38
39
40class SpecialValue(object):
41 """ Encapsulates a value in the cache in place of a normal value. """
42 def __init__(self, value):
43 self.value = value
44 def get(self):
45 return self.value
46
47class FailedValue(SpecialValue):
48 """ Special value that encapsulates an exception instead of a value. """
49 def __init__(self, exception):
50 self.exception = exception
51 def get(self):
52 raise self.exception
53
54def _check_value(value):
55 """ Return `value`, or call its getter if `value` is a :class:`SpecialValue`. """
56 return value.get() if isinstance(value, SpecialValue) else value
57
58
59def default(value):
60 """ Return a compute function that provides a constant default value. """
61 def compute(field, records):
62 for record in records:
63 record[field.name] = value
64
65 return compute
66
67
68def compute_related(field, records):
69 """ Compute the related `field` on `records`. """
70 scope = records._scope
71 sudo_scope = scope.sudo()
72 for record in records:
73 # bypass access rights check when traversing the related path
74 value = record.scoped(sudo_scope) if record.id else record
75 for name in field.related:
76 value = value[name]
77 # re-scope the resulting value
78 if isinstance(value, BaseModel):
79 value = value.scoped(scope)
80 record[field.name] = value
81
82def inverse_related(field, records):
83 """ Inverse the related `field` on `records`. """
84 for record in records:
85 other = record
86 for name in field.related[:-1]:
87 other = other[name]
88 if other:
89 other[field.related[-1]] = record[field.name]
90
91def search_related(field, operator, value):
92 """ Determine the domain to search on `field`. """
93 return [('.'.join(field.related), operator, value)]
94
95
96class MetaField(type):
97 """ Metaclass for field classes. """
98 by_type = {}
99
100 def __init__(cls, name, bases, attrs):
101 super(MetaField, cls).__init__(name, bases, attrs)
102 if cls.type:
103 cls.by_type[cls.type] = cls
104
105
106class Field(object):
107 """ Base class of all fields. """
108 __metaclass__ = MetaField
109
110 interface_for = None # the column or field interfaced by self, if any
111
112 name = None # name of the field
113 model_name = None # name of the model of this field
114 type = None # type of the field (string)
115 relational = False # whether the field is a relational one
116 inverse_field = None # inverse field (object), if it exists
117
118 store = True # whether the field is stored in database
119 depends = () # collection of field dependencies
120 compute = None # name of model method that computes value
121 inverse = None # name of model method that inverses field
122 search = None # name of model method that searches on field
123 related = None # sequence of field names, for related fields
124
125 string = None # field label
126 help = None # field tooltip
127 readonly = False
128 required = False
129 states = None
130 groups = False # csv list of group xml ids
131
132 # arguments passed to column class by to_column()
133 _column_string = attrgetter('string')
134 _column_help = attrgetter('help')
135 _column_readonly = attrgetter('readonly')
136 _column_required = attrgetter('required')
137 _column_states = attrgetter('states')
138 _column_groups = attrgetter('groups')
139
140 # attributes copied from related field by setup_related()
141 _related_string = attrgetter('string')
142 _related_help = attrgetter('help')
143 _related_readonly = attrgetter('readonly')
144 _related_required = attrgetter('required')
145 _related_states = attrgetter('states')
146 _related_groups = attrgetter('groups')
147
148 # attributes exported by get_description()
149 _description_depends = attrgetter('depends')
150 _description_related = attrgetter('related')
151 _description_string = attrgetter('string')
152 _description_help = attrgetter('help')
153 _description_readonly = attrgetter('readonly')
154 _description_required = attrgetter('required')
155 _description_states = attrgetter('states')
156 _description_groups = attrgetter('groups')
157
158 def __init__(self, string=None, **kwargs):
159 kwargs['string'] = string
160 for attr, value in kwargs.iteritems():
161 setattr(self, attr, value)
162
163 def reset(self):
164 """ Prepare `self` for a new setup. This resets all lazy properties. """
165 lazy_property.reset_all(self)
166
167 def copy(self, **kwargs):
168 """ make a copy of `self`, possibly modified with parameters `kwargs` """
169 field = copy(self)
170 for attr, value in kwargs.iteritems():
171 setattr(field, attr, value)
172 # Note: lazy properties will be recomputed later thanks to reset()
173 return field
174
175 def set_model_name(self, model_name, name):
176 """ assign the model and field names of `self` """
177 self.model_name = model_name
178 self.name = name
179 if not self.string:
180 self.string = name.replace('_', ' ').capitalize()
181
182 @lazy_property
183 def model(self):
184 """ return the model instance of `self` """
185 return scope[self.model_name]
186
187 def __str__(self):
188 return "%s.%s" % (self.model_name, self.name)
189
190 def get_description(self):
191 """ Return a dictionary that describes the field `self`. """
192 desc = {'type': self.type, 'store': self.store}
193 for attr in dir(self):
194 if attr.startswith('_description_'):
195 value = getattr(self, attr)(self)
196 if value:
197 desc[attr[13:]] = value
198 return desc
199
200 def to_column(self):
201 """ return a low-level field object corresponding to `self` """
202 assert self.store
203 if self.interface_for:
204 assert isinstance(self.interface_for, fields._column)
205 return self.interface_for
206
207 _logger.debug("Create fields._column for Field %s", self)
208 args = {}
209 for attr in dir(self):
210 if attr.startswith('_column_'):
211 args[attr[8:]] = getattr(self, attr)(self)
212 return getattr(fields, self.type)(**args)
213
214 #
215 # Conversion of values
216 #
217
218 def null(self):
219 """ return the null value for this field """
220 return False
221
222 def convert_to_cache(self, value):
223 """ convert `value` to the cache level; `value` may come from an
224 assignment, or have the format of methods :meth:`BaseModel.read` or
225 :meth:`BaseModel.write`
226 """
227 return value
228
229 def convert_to_read(self, value, use_name_get=True):
230 """ convert `value` from the cache to a value as returned by method
231 :meth:`BaseModel.read`
232 """
233 return value
234
235 def convert_to_write(self, value):
236 """ convert `value` from the cache to a valid value for method
237 :meth:`BaseModel.write`
238 """
239 return self.convert_to_read(value)
240
241 def convert_to_export(self, value):
242 """ convert `value` from the cache to a valid value for export. """
243 return bool(value) and ustr(value)
244
245 def convert_to_display_name(self, value):
246 """ convert `value` from the cache to a suitable display name. """
247 return ustr(value)
248
249 #
250 # Getter/setter methods
251 #
252
253 def __get__(self, record, owner):
254 """ return the value of field `self` on `record` """
255 if record is None:
256 return self # the field is accessed through the owner class
257
258 cache, id = record._scope.cache, record._id
259
260 try:
261 return _check_value(cache[self][id])
262 except KeyError:
263 pass
264
265 # cache miss, retrieve value
266 with record._scope:
267 if id:
268 # normal record -> read or compute value for this field
269 self.determine_value(record[0])
270 elif record:
271 # new record -> compute default value for this field
272 record.add_default_value(self.name)
273 else:
274 # null record -> return the null value for this field
275 return self.null()
276
277 # the result should be in cache now
278 return _check_value(cache[self][id])
279
280 def __set__(self, record, value):
281 """ set the value of field `self` on `record` """
282 if not record:
283 raise Warning("Null record %s may not be assigned" % record)
284
285 with record._scope as _scope:
286 # adapt value to the cache level
287 value = self.convert_to_cache(value)
288
289 # notify the change, which may cause cache invalidation
290 if _scope.draft or not record._id:
291 self.modified_draft(record)
292 else:
293 record.write({self.name: self.convert_to_write(value)})
294
295 # store the value in cache
296 _scope.cache[self][record._id] = value
297
298 #
299 # Management of the computation of field values.
300 #
301
302 @lazy_property
303 def _compute_function(self):
304 """ Return a function to call with records to compute this field. """
305 if isinstance(self.compute, basestring):
306 return getattr(type(self.model), self.compute)
307 elif callable(self.compute):
308 return partial(self.compute, self)
309 else:
310 raise Warning("No way to compute field %s" % self)
311
312 @lazy_property
313 def _compute_one(self):
314 """ Test whether the compute function has the decorator ``@one``. """
315 from openerp import one
316 return getattr(self._compute_function, '_api', None) is one
317
318 def compute_value(self, records, check_exists=False):
319 """ Invoke the compute method on `records`. If `check` is ``True``, the
320 method filters out non-existing records before computing them.
321 """
322 # if required, keep new and existing records only
323 if check_exists:
324 all_recs = records
325 new_recs = [rec for rec in records if not rec.id]
326 records = sum(new_recs, records.exists())
327
328 # mark non-existing records in cache
329 exc = MissingError("Computing a field on non-existing records.")
330 (all_recs - records)._update_cache(FailedValue(exc))
331
332 # mark the field failed in cache, so that access before computation
333 # raises an exception
334 exc = Warning("Field %s is accessed before being computed." % self)
335 records._update_cache({self.name: FailedValue(exc)})
336
337 self._compute_function(records)
338
339 def determine_value(self, record):
340 """ Determine the value of `self` for `record`. """
341 if self.store:
342 # recompute field on record if required
343 recs_todo = scope.recomputation.todo(self)
344 if record in recs_todo:
345 # execute the compute method in NON-DRAFT mode, so that assigned
346 # fields are written to the database
347 if self._compute_one:
348 self.compute_value(record, check_exists=True)
349 else:
350 self.compute_value(recs_todo, check_exists=True)
351 else:
352 record._prefetch_field(self.name)
353
354 else:
355 # execute the compute method in DRAFT mode, so that assigned fields
356 # are not written to the database
357 with record._scope.draft():
358 if self._compute_one:
359 self.compute_value(record)
360 else:
361 record._in_cache()
362 recs = record._in_cache_without(self.name)
363 self.compute_value(recs, check_exists=True)
364
365 def determine_default(self, record):
366 """ determine the default value of field `self` on `record` """
367 record._update_cache({self.name: SpecialValue(self.null())})
368 if self.compute:
369 self.compute_value(record)
370
371 def determine_inverse(self, records):
372 """ Given the value of `self` on `records`, inverse the computation. """
373 if isinstance(self.inverse, basestring):
374 getattr(records, self.inverse)()
375 elif callable(self.inverse):
376 self.inverse(self, records)
377
378 def determine_domain(self, operator, value):
379 """ Return a domain representing a condition on `self`. """
380 if isinstance(self.search, basestring):
381 return getattr(self.model.browse(), self.search)(operator, value)
382 elif callable(self.search):
383 return self.search(self, operator, value)
384 else:
385 return [(self.name, operator, value)]
386
387 #
388 # Setup of related fields.
389 #
390
391 @lazy_property
392 def related_field(self):
393 """ return the related field corresponding to `self` """
394 if self.related:
395 model = self.model
396 for name in self.related[:-1]:
397 model = model[name]
398 return model._fields[self.related[-1]]
399 return None
400
401 def setup_related(self):
402 """ Setup the attributes of the related field `self`. """
403 assert self.related
404 # fix the type of self.related if necessary
405 if isinstance(self.related, basestring):
406 self.related = tuple(self.related.split('.'))
407
408 # check type consistency
409 field = self.related_field
410 if self.type != field.type:
411 raise Warning("Type of related field %s is inconsistent with %s" % (self, field))
412
413 # determine dependencies, compute, inverse, and search
414 self.depends = ('.'.join(self.related),)
415 self.compute = compute_related
416 self.inverse = inverse_related
417 self.search = search_related
418
419 # copy attributes from field to self (readonly, required, etc.)
420 field.setup()
421 for attr in dir(self):
422 if attr.startswith('_related_'):
423 if not getattr(self, attr[9:]):
424 setattr(self, attr[9:], getattr(self, attr)(field))
425
426 #
427 # Field setup.
428 #
429 # Recomputation of computed fields: each field stores a set of triggers
430 # (`field`, `path`); when the field is modified, it invalidates the cache of
431 # `field` and registers the records to recompute based on `path`. See method
432 # `modified` below for details.
433 #
434
435 @lazy_property
436 def _triggers(self):
437 """ List of pairs (`field`, `path`), where `field` is a field to
438 recompute, and `path` is the dependency between `field` and `self`
439 (dot-separated sequence of field names between `field.model` and
440 `self.model`).
441 """
442 return []
443
444 def setup(self):
445 """ Complete the setup of `self`: make it process its dependencies and
446 store triggers on other fields to be recomputed.
447 """
448 return self._setup # trigger _setup() if not done yet
449
450 @lazy_property
451 def _setup(self):
452 if self.related:
453 # setup all attributes of related field
454 self.setup_related()
455 else:
456 # retrieve dependencies from compute method
457 if isinstance(self.compute, basestring):
458 method = getattr(type(self.model), self.compute)
459 else:
460 method = self.compute
461
462 depends = getattr(method, '_depends', ())
463 self.depends = depends(self.model) if callable(depends) else depends
464
465 # put invalidation/recomputation triggers on dependencies
466 for path in self.depends:
467 self._depends_on_model(self.model, [], path.split('.'))
468
469 def _depends_on_model(self, model, path0, path1):
470 """ Make `self` depend on `model`; `path0 + path1` is a dependency of
471 `self`, and `path0` is the sequence of field names from `self.model`
472 to `model`.
473 """
474 name, tail = path1[0], path1[1:]
475 if name == '*':
476 # special case: add triggers on all fields of model
477 fields = model._fields.values()
478 if not path0:
479 fields.remove(self) # self cannot depend directly on itself
480 else:
481 fields = (model._fields[name],)
482
483 for field in fields:
484 field._add_trigger_for(self, path0, tail)
485
486 def _add_trigger_for(self, field, path0, path1):
487 """ Add a trigger on `self` to recompute `field`; `path0` is the
488 sequence of field names from `field.model` to `self.model`; ``path0
489 + [self.name] + path1`` is a dependency of `field`.
490 """
491 self._triggers.append((field, '.'.join(path0) if path0 else 'id'))
492 _logger.debug("Add trigger on field %s to recompute field %s", self, field)
493
494 #
495 # Notification when fields are modified
496 #
497
498 def modified(self, records):
499 """ Notify that field `self` has been modified on `records`: prepare the
500 fields/records to recompute, and return a spec indicating what to
501 invalidate.
502 """
503 # invalidate cache for self
504 ids = records.unbrowse()
505 spec = [(self, ids)]
506
507 # invalidate the fields that depend on self, and prepare their
508 # recomputation
509 for field, path in self._triggers:
510 if field.store:
511 with scope(user=SUPERUSER_ID, context={'active_test': False}):
512 target = field.model.search([(path, 'in', ids)])
513 spec.append((field, target.unbrowse()))
514 scope.recomputation.todo(field, target)
515 else:
516 spec.append((field, None))
517
518 return spec
519
520 def modified_draft(self, records):
521 """ Same as :meth:`modified`, but in draft mode. """
522 # invalidate self and dependent fields on records only
523 model_name = self.model_name
524 fields = [self] + [f for f, _ in self._triggers if f.model_name == model_name]
525 ids = records._ids
526 scope.invalidate([(f, ids) for f in fields])
527
528
529class Boolean(Field):
530 """ Boolean field. """
531 type = 'boolean'
532
533 def convert_to_cache(self, value):
534 return bool(value)
535
536 def convert_to_export(self, value):
537 return ustr(value)
538
539
540class Integer(Field):
541 """ Integer field. """
542 type = 'integer'
543
544 def convert_to_cache(self, value):
545 return int(value or 0)
546
547
548class Float(Field):
549 """ Float field. """
550 type = 'float'
551 _digits = None
552
553 _column_digits = staticmethod(lambda self: not callable(self._digits) and self._digits)
554 _column_digits_compute = staticmethod(lambda self: callable(self._digits) and self._digits)
555
556 _related_digits = attrgetter('digits')
557 _description_digits = attrgetter('digits')
558
559 def __init__(self, string=None, digits=None, **kwargs):
560 self._digits = digits
561 super(Float, self).__init__(string=string, **kwargs)
562
563 @lazy_property
564 def digits(self):
565 return self._digits(scope.cr) if callable(self._digits) else self._digits
566
567 def convert_to_cache(self, value):
568 # apply rounding here, otherwise value in cache may be wrong!
569 if self.digits:
570 return float_round(float(value or 0.0), precision_digits=self.digits[1])
571 else:
572 return float(value or 0.0)
573
574
575class _String(Field):
576 """ Abstract class for string fields. """
577 translate = False
578
579 _column_translate = attrgetter('translate')
580 _related_translate = attrgetter('translate')
581 _description_translate = attrgetter('translate')
582
583
584class Char(_String):
585 """ Char field. """
586 type = 'char'
587 size = None
588
589 _column_size = attrgetter('size')
590 _related_size = attrgetter('size')
591 _description_size = attrgetter('size')
592
593 def convert_to_cache(self, value):
594 return bool(value) and ustr(value)[:self.size]
595
596
597class Text(_String):
598 """ Text field. """
599 type = 'text'
600
601 def convert_to_cache(self, value):
602 return bool(value) and ustr(value)
603
604
605class Html(_String):
606 """ Html field. """
607 type = 'html'
608
609 def convert_to_cache(self, value):
610 return bool(value) and html_sanitize(value)
611
612
613class Date(Field):
614 """ Date field. """
615 type = 'date'
616
617 def convert_to_cache(self, value):
618 if isinstance(value, (date, datetime)):
619 value = value.strftime(DATE_FORMAT)
620 elif value:
621 # check the date format
622 value = value[:DATE_LENGTH]
623 datetime.strptime(value, DATE_FORMAT)
624 return value or False
625
626
627class Datetime(Field):
628 """ Datetime field. """
629 type = 'datetime'
630
631 def convert_to_cache(self, value):
632 if isinstance(value, (date, datetime)):
633 value = value.strftime(DATETIME_FORMAT)
634 elif value:
635 # check the datetime format
636 value = value[:DATETIME_LENGTH]
637 datetime.strptime(value, DATETIME_FORMAT)
638 return value or False
639
640
641class Binary(Field):
642 """ Binary field. """
643 type = 'binary'
644
645
646class Selection(Field):
647 """ Selection field. """
648 type = 'selection'
649 selection = None # [(value, string), ...], model method or method name
650
651 _description_selection = staticmethod(lambda self: self.get_selection())
652
653 def __init__(self, selection, string=None, **kwargs):
654 """ Selection field.
655
656 :param selection: specifies the possible values for this field.
657 It is given as either a list of pairs (`value`, `string`), or a
658 model method, or a method name.
659 """
660 if callable(selection):
661 from openerp import api
662 selection = api.expected(api.model, selection)
663 super(Selection, self).__init__(selection=selection, string=string, **kwargs)
664
665 @staticmethod
666 def _column_selection(self):
667 if isinstance(self.selection, basestring):
668 method = self.selection
669 return lambda self, *a, **kw: getattr(self, method)(*a, **kw)
670 else:
671 return self.selection
672
673 def setup_related(self):
674 super(Selection, self).setup_related()
675 # selection must be computed on related field
676 self.selection = lambda model: self.related_field.get_selection()
677
678 def get_selection(self):
679 """ return the selection list (pairs (value, string)) """
680 value = self.selection
681 if isinstance(value, basestring):
682 value = getattr(self.model, value)()
683 elif callable(value):
684 value = value(self.model)
685 return value
686
687 def get_values(self):
688 """ return a list of the possible values """
689 return [item[0] for item in self.get_selection()]
690
691 def convert_to_cache(self, value):
692 if value in self.get_values():
693 return value
694 elif not value:
695 return False
696 raise ValueError("Wrong value for %s: %r" % (self, value))
697
698 def convert_to_export(self, value):
699 if not isinstance(self.selection, list):
700 # FIXME: this reproduces an existing buggy behavior!
701 return value
702 for item in self.get_selection():
703 if item[0] == value:
704 return item[1]
705 return False
706
707
708class Reference(Selection):
709 """ Reference field. """
710 type = 'reference'
711 size = 128
712
713 _column_size = attrgetter('size')
714 _related_size = attrgetter('size')
715
716 def __init__(self, selection, string=None, **kwargs):
717 """ Reference field.
718
719 :param selection: specifies the possible model names for this field.
720 It is given as either a list of pairs (`value`, `string`), or a
721 model method, or a method name.
722 """
723 super(Reference, self).__init__(selection=selection, string=string, **kwargs)
724
725 def convert_to_cache(self, value):
726 if isinstance(value, BaseModel):
727 if value._name in self.get_values() and len(value) <= 1:
728 return value.scoped() or False
729 elif isinstance(value, basestring):
730 res_model, res_id = value.split(',')
731 return scope[res_model].browse(int(res_id))
732 elif not value:
733 return False
734 raise ValueError("Wrong value for %s: %r" % (self, value))
735
736 def convert_to_read(self, value, use_name_get=True):
737 return "%s,%s" % (value._name, value.id) if value else False
738
739 def convert_to_export(self, value):
740 return bool(value) and value.name_get()[0][1]
741
742 def convert_to_display_name(self, value):
743 return ustr(value and value.display_name)
744
745
746class _Relational(Field):
747 """ Abstract class for relational fields. """
748 relational = True
749 comodel_name = None # name of model of values
750 domain = None # domain for searching values
751 context = None # context for searching values
752
753 _column_obj = attrgetter('comodel_name')
754 _column_domain = attrgetter('domain')
755 _column_context = attrgetter('context')
756
757 _description_relation = attrgetter('comodel_name')
758 _description_domain = staticmethod(lambda self: \
759 self.domain(self.model) if callable(self.domain) else self.domain)
760 _description_context = attrgetter('context')
761
762 def __init__(self, **kwargs):
763 super(_Relational, self).__init__(**kwargs)
764 if callable(self.domain):
765 from openerp import api
766 self.domain = api.expected(api.model, self.domain)
767
768 @lazy_property
769 def comodel(self):
770 """ return the comodel instance of `self` """
771 return scope[self.comodel_name]
772
773 def null(self):
774 return self.comodel.browse()
775
776 def _add_trigger_for(self, field, path0, path1):
777 # overridden to traverse relations and manage inverse fields
778 Field._add_trigger_for(self, field, path0, [])
779
780 if self.inverse_field:
781 # add trigger on inverse field, too
782 Field._add_trigger_for(self.inverse_field, field, path0 + [self.name], [])
783
784 if path1:
785 # recursively traverse the dependency
786 field._depends_on_model(self.comodel, path0 + [self.name], path1)
787
788 def modified(self, records):
789 # Invalidate cache for self.inverse_field, too. Note that recomputation
790 # of fields that depend on self.inverse_field is already covered by the
791 # triggers (see above).
792 spec = super(_Relational, self).modified(records)
793 if self.inverse_field:
794 spec.append((self.inverse_field, None))
795 return spec
796
797
798class Many2one(_Relational):
799 """ Many2one field. """
800 type = 'many2one'
801 ondelete = 'set null' # what to do when value is deleted
802 auto_join = False # whether joins are generated upon search
803 delegate = False # whether self implements delegation
804
805 _column_ondelete = attrgetter('ondelete')
806 _column_auto_join = attrgetter('auto_join')
807
808 def __init__(self, comodel_name, string=None, **kwargs):
809 super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
810
811 @lazy_property
812 def inverse_field(self):
813 for field in self.comodel._fields.itervalues():
814 if isinstance(field, One2many) and field.inverse_field == self:
815 return field
816 return None
817
818 @lazy_property
819 def inherits(self):
820 """ Whether `self` implements inheritance between model and comodel. """
821 return self.name in self.model._inherits.itervalues()
822
823 def convert_to_cache(self, value):
824 if isinstance(value, BaseModel):
825 if value._name == self.comodel_name and len(value) <= 1:
826 return value.scoped()
827 raise ValueError("Wrong value for %s: %r" % (self, value))
828 elif isinstance(value, tuple):
829 return self.comodel.browse(value[0])
830 elif isinstance(value, dict):
831 return self.comodel.new(value)
832 else:
833 return self.comodel.browse(value)
834
835 def convert_to_read(self, value, use_name_get=True):
836 if use_name_get and value:
837 # evaluate name_get() in sudo scope, because the visibility of a
838 # many2one field value (id and name) depends on the current record's
839 # access rights, and not the value's access rights.
840 with value._scope.sudo():
841 return value.name_get()[0]
842 else:
843 return value.id
844
845 def convert_to_write(self, value):
846 return value.id
847
848 def convert_to_export(self, value):
849 return bool(value) and value.name_get()[0][1]
850
851 def convert_to_display_name(self, value):
852 return ustr(value.display_name)
853
854 def determine_default(self, record):
855 super(Many2one, self).determine_default(record)
856 if self.inherits:
857 # special case: fields that implement inheritance between models
858 value = record[self.name]
859 if not value:
860 # the default value cannot be null, use a new record instead
861 record[self.name] = self.comodel.new()
862
863
864class _RelationalMulti(_Relational):
865 """ Abstract class for relational fields *2many. """
866
867 def convert_to_cache(self, value):
868 if isinstance(value, BaseModel):
869 if value._name == self.comodel_name:
870 return value.scoped()
871 elif isinstance(value, list):
872 # value is a list of record ids or commands
873 result = self.comodel.browse()
874 for command in value:
875 if isinstance(command, (tuple, list)):
876 if command[0] == 0:
877 result += self.comodel.new(command[2])
878 elif command[0] == 1:
879 record = self.comodel.browse(command[1])
880 record.update(command[2])
881 result += record
882 elif command[0] == 2:
883 pass
884 elif command[0] == 3:
885 pass
886 elif command[0] == 4:
887 result += self.comodel.browse(command[1])
888 elif command[0] == 5:
889 result = self.comodel.browse()
890 elif command[0] == 6:
891 result = self.comodel.browse(command[2])
892 elif isinstance(command, dict):
893 result += self.comodel.new(command)
894 else:
895 result += self.comodel.browse(command)
896 return result
897 elif not value:
898 return self.null()
899 raise ValueError("Wrong value for %s: %s" % (self, value))
900
901 def convert_to_read(self, value, use_name_get=True):
902 return value.unbrowse()
903
904 def convert_to_write(self, value):
905 result = [(5,)]
906 for record in value:
907 # TODO: modified record (1, id, values)
908 if not record.id:
909 values = record._convert_to_write(record._get_cache())
910 result.append((0, 0, values))
911 else:
912 result.append((4, record.id))
913 return result
914
915 def convert_to_export(self, value):
916 return bool(value) and ','.join(name for id, name in value.name_get())
917
918 def convert_to_display_name(self, value):
919 raise NotImplementedError()
920
921
922class One2many(_RelationalMulti):
923 """ One2many field. """
924 type = 'one2many'
925 inverse_name = None # name of the inverse field
926 auto_join = False # whether joins are generated upon search
927 limit = None # optional limit to use upon read
928
929 _column_fields_id = attrgetter('inverse_name')
930 _column_auto_join = attrgetter('auto_join')
931 _column_limit = attrgetter('limit')
932
933 _description_relation_field = attrgetter('inverse_name')
934
935 def __init__(self, comodel_name, inverse_name=None, string=None, **kwargs):
936 super(One2many, self).__init__(
937 comodel_name=comodel_name, inverse_name=inverse_name, string=string, **kwargs)
938
939 @lazy_property
940 def inverse_field(self):
941 return self.inverse_name and self.comodel._fields[self.inverse_name]
942
943
944class Many2many(_RelationalMulti):
945 """ Many2many field. """
946 type = 'many2many'
947 relation = None # name of table
948 column1 = None # column of table referring to model
949 column2 = None # column of table referring to comodel
950 limit = None # optional limit to use upon read
951
952 _column_rel = attrgetter('relation')
953 _column_id1 = attrgetter('column1')
954 _column_id2 = attrgetter('column2')
955 _column_limit = attrgetter('limit')
956
957 def __init__(self, comodel_name, relation=None, column1=None, column2=None,
958 string=None, **kwargs):
959 super(Many2many, self).__init__(comodel_name=comodel_name, relation=relation,
960 column1=column1, column2=column2, string=string, **kwargs)
961
962 @lazy_property
963 def inverse_field(self):
964 if not self.compute:
965 expected = (self.relation, self.column2, self.column1)
966 for field in self.comodel._fields.itervalues():
967 if isinstance(field, Many2many) and \
968 (field.relation, field.column1, field.column2) == expected:
969 return field
970 return None
971
972
973class Id(Field):
974 """ Special case for field 'id'. """
975 store = False
976 readonly = True
977
978 def to_column(self):
979 raise NotImplementedError()
980
981 def __get__(self, instance, owner):
982 if instance is None:
983 return self # the field is accessed through the class owner
984 return instance._id
985
986 def __set__(self, instance, value):
987 raise NotImplementedError()
988
989
990# imported here to avoid dependency cycle issues
991from openerp import SUPERUSER_ID
992from openerp.exceptions import Warning, MissingError
993from openerp.osv import fields
994from openerp.osv.orm import BaseModel
995from openerp.osv.scope import proxy as scope
0996
=== modified file 'openerp/osv/orm.py'
--- openerp/osv/orm.py 2014-01-17 12:02:35 +0000
+++ openerp/osv/orm.py 2014-01-22 16:19:54 +0000
@@ -21,46 +21,49 @@
2121
2222
23"""23"""
24 Object relational mapping to database (postgresql) module24 Object Relational Mapping module:
25 * Hierarchical structure25 * Hierarchical structure
26 * Constraints consistency, validations26 * Constraints consistency and validation
27 * Object meta Data depends on its status27 * Object metadata depends on its status
28 * Optimised processing by complex query (multiple actions at once)28 * Optimised processing by complex query (multiple actions at once)
29 * Default fields value29 * Default field values
30 * Permissions optimisation30 * Permissions optimisation
31 * Persistant object: DB postgresql31 * Persistant object: DB postgresql
32 * Datas conversions32 * Data conversion
33 * Multi-level caching system33 * Multi-level caching system
34 * 2 different inheritancies34 * Two different inheritance mechanisms
35 * Fields:35 * Rich set of field types:
36 - classicals (varchar, integer, boolean, ...)36 - classical (varchar, integer, boolean, ...)
37 - relations (one2many, many2one, many2many)37 - relational (one2many, many2one, many2many)
38 - functions38 - functional
3939
40"""40"""
4141
42import calendar42import calendar
43import collections43from collections import defaultdict, Iterable
44import copy44import copy
45import datetime45import datetime
46import itertools46import itertools
47import logging47import logging
48import operator48import operator
49import pickle49import pickle
50import functools
50import re51import re
51import simplejson52import simplejson
52import time53import time
53import traceback
54import types
5554
56import babel.dates55import babel.dates
57import dateutil.parser56import dateutil.parser
58import psycopg257import psycopg2
59from lxml import etree58from lxml import etree
6059
60import api
61from scope import proxy as scope_proxy
61import fields62import fields
62import openerp63import openerp
63import openerp.tools as tools64import openerp.tools as tools
65from openerp.exceptions import except_orm, AccessError, MissingError
66from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
64from openerp.tools.config import config67from openerp.tools.config import config
65from openerp.tools.misc import CountingStream68from openerp.tools.misc import CountingStream
66from openerp.tools.safe_eval import safe_eval as eval69from openerp.tools.safe_eval import safe_eval as eval
@@ -238,6 +241,10 @@
238def intersect(la, lb):241def intersect(la, lb):
239 return filter(lambda x: x in lb, la)242 return filter(lambda x: x in lb, la)
240243
244def same_name(f, g):
245 """ Test whether functions `f` and `g` are identical or have the same name """
246 return f == g or getattr(f, '__name__', 0) == getattr(g, '__name__', 1)
247
241def fix_import_export_id_paths(fieldname):248def fix_import_export_id_paths(fieldname):
242 """249 """
243 Fixes the id fields in import and exports, and splits field paths250 Fixes the id fields in import and exports, and splits field paths
@@ -251,303 +258,6 @@
251 fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)258 fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)
252 return fixed_external_id.split('/')259 return fixed_external_id.split('/')
253260
254class except_orm(Exception):
255 def __init__(self, name, value):
256 self.name = name
257 self.value = value
258 self.args = (name, value)
259
260class BrowseRecordError(Exception):
261 pass
262
263class browse_null(object):
264 """ Readonly python database object browser
265 """
266
267 def __init__(self):
268 self.id = False
269
270 def __getitem__(self, name):
271 return None
272
273 def __getattr__(self, name):
274 return None # XXX: return self ?
275
276 def __int__(self):
277 return False
278
279 def __str__(self):
280 return ''
281
282 def __nonzero__(self):
283 return False
284
285 def __unicode__(self):
286 return u''
287
288 def __iter__(self):
289 raise NotImplementedError("Iteration is not allowed on %s" % self)
290
291
292#
293# TODO: execute an object method on browse_record_list
294#
295class browse_record_list(list):
296 """ Collection of browse objects
297
298 Such an instance will be returned when doing a ``browse([ids..])``
299 and will be iterable, yielding browse() objects
300 """
301
302 def __init__(self, lst, context=None):
303 if not context:
304 context = {}
305 super(browse_record_list, self).__init__(lst)
306 self.context = context
307
308
309class browse_record(object):
310 """ An object that behaves like a row of an object's table.
311 It has attributes after the columns of the corresponding object.
312
313 Examples::
314
315 uobj = pool.get('res.users')
316 user_rec = uobj.browse(cr, uid, 104)
317 name = user_rec.name
318 """
319
320 def __init__(self, cr, uid, id, table, cache, context=None,
321 list_class=browse_record_list, fields_process=None):
322 """
323 :param table: the browsed object (inherited from orm)
324 :param dict cache: a dictionary of model->field->data to be shared
325 across browse objects, thus reducing the SQL
326 read()s. It can speed up things a lot, but also be
327 disastrous if not discarded after write()/unlink()
328 operations
329 :param dict context: dictionary with an optional context
330 """
331 if fields_process is None:
332 fields_process = {}
333 if context is None:
334 context = {}
335 self._list_class = list_class
336 self._cr = cr
337 self._uid = uid
338 self._id = id
339 self._table = table # deprecated, use _model!
340 self._model = table
341 self._table_name = self._table._name
342 self.__logger = logging.getLogger('openerp.osv.orm.browse_record.' + self._table_name)
343 self._context = context
344 self._fields_process = fields_process
345
346 cache.setdefault(table._name, {})
347 self._data = cache[table._name]
348
349# if not (id and isinstance(id, (int, long,))):
350# raise BrowseRecordError(_('Wrong ID for the browse record, got %r, expected an integer.') % (id,))
351# if not table.exists(cr, uid, id, context):
352# raise BrowseRecordError(_('Object %s does not exists') % (self,))
353
354 if id not in self._data:
355 self._data[id] = {'id': id}
356
357 self._cache = cache
358
359 def __getitem__(self, name):
360 if name == 'id':
361 return self._id
362
363 if name not in self._data[self._id]:
364 # build the list of fields we will fetch
365
366 # fetch the definition of the field which was asked for
The diff has been truncated for viewing.