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