Merge lp:~openerp-dev/openobject-server/trunk-apiculture into lp:openobject-server
- trunk-apiculture
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~openerp-dev/openobject-server/trunk-apiculture |
Merge into: | lp:openobject-server |
Diff against target: |
12600 lines (+6838/-2509) (has conflicts) 88 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 (+18/-3) openerp/addons/base/__openerp__.py (+1/-2) openerp/addons/base/base_menu.xml (+4/-0) openerp/addons/base/ir/ir_actions.py (+8/-8) openerp/addons/base/ir/ir_attachment.py (+2/-2) openerp/addons/base/ir/ir_cron.py (+23/-21) openerp/addons/base/ir/ir_mail_server.py (+3/-6) openerp/addons/base/ir/ir_model.py (+39/-44) openerp/addons/base/ir/ir_qweb.py (+5/-5) 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 (+63/-60) openerp/addons/base/ir/ir_ui_view.py (+42/-17) openerp/addons/base/ir/ir_values.py (+27/-2) openerp/addons/base/module/module.py (+65/-45) openerp/addons/base/res/ir_property.py (+3/-4) openerp/addons/base/res/res_company.py (+3/-3) openerp/addons/base/res/res_config.py (+3/-3) openerp/addons/base/res/res_currency.py (+61/-20) openerp/addons/base/res/res_partner.py (+147/-144) openerp/addons/base/res/res_users.py (+32/-14) openerp/addons/base/security/base_security.xml (+3/-0) openerp/addons/base/tests/__init__.py (+1/-2) openerp/addons/base/tests/base_test.yml (+1/-1) openerp/addons/base/tests/test_acl.py (+34/-20) openerp/addons/base/tests/test_api.py (+442/-0) openerp/addons/base/tests/test_ir_rule.yml (+1/-1) openerp/addons/base/tests/test_orm.py (+14/-0) openerp/addons/base/tests/test_osv_expression.yml (+2/-2) openerp/addons/base/tests/test_views.py (+4/-0) openerp/addons/test_impex/models.py (+55/-39) openerp/addons/test_impex/tests/test_export.py (+7/-10) openerp/addons/test_impex/tests/test_import.py (+5/-5) openerp/addons/test_impex/tests/test_load.py (+6/-6) openerp/addons/test_inherit/__init__.py (+3/-0) openerp/addons/test_inherit/__openerp__.py (+15/-0) openerp/addons/test_inherit/ir.model.access.csv (+1/-0) openerp/addons/test_inherit/models.py (+29/-0) openerp/addons/test_inherit/tests/__init__.py (+12/-0) openerp/addons/test_inherit/tests/test_inherit.py (+17/-0) openerp/addons/test_new_api/__init__.py (+2/-0) openerp/addons/test_new_api/__openerp__.py (+19/-0) openerp/addons/test_new_api/demo_data.xml (+30/-0) openerp/addons/test_new_api/ir.model.access.csv (+6/-0) openerp/addons/test_new_api/models.py (+183/-0) openerp/addons/test_new_api/tests/__init__.py (+18/-0) openerp/addons/test_new_api/tests/test_attributes.py (+25/-0) openerp/addons/test_new_api/tests/test_field_conversions.py (+11/-0) openerp/addons/test_new_api/tests/test_new_fields.py (+346/-0) openerp/addons/test_new_api/tests/test_onchange.py (+159/-0) openerp/addons/test_new_api/tests/test_related.py (+4/-57) openerp/addons/test_new_api/views.xml (+127/-0) openerp/addons/test_workflow/models.py (+8/-7) openerp/addons/test_workflow/tests/test_workflow.py (+1/-1) openerp/exceptions.py (+15/-1) openerp/http.py (+1/-0) openerp/modules/loading.py (+2/-0) openerp/modules/module.py (+1/-1) openerp/modules/registry.py (+58/-42) openerp/osv/__init__.py (+3/-2) openerp/osv/api.py (+624/-0) openerp/osv/env.py (+196/-0) openerp/osv/expression.py (+125/-104) openerp/osv/fields.py (+125/-108) openerp/osv/fields2.py (+1161/-0) openerp/osv/orm.py (+1926/-1364) openerp/report/custom.py (+5/-6) openerp/report/print_xml.py (+12/-30) openerp/report/report_sxw.py (+21/-97) openerp/service/model.py (+1/-1) openerp/service/security.py (+1/-1) openerp/service/wsgi_server.py (+9/-8) openerp/tests/common.py (+3/-0) openerp/tools/__init__.py (+1/-0) openerp/tools/cache.py (+99/-101) openerp/tools/misc.py (+24/-0) openerp/tools/test_reports.py (+1/-1) openerp/tools/translate.py (+45/-41) openerp/tools/yaml_import.py (+52/-26) openerp/tools/yaml_tag.py (+1/-0) openerp/workflow/workitem.py (+3/-4) setup.py (+2/-0) Text conflict in openerp/addons/base/security/base_security.xml |
To merge this branch: | bzr merge lp:~openerp-dev/openobject-server/trunk-apiculture |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christophe Simonis (OpenERP) | Needs Fixing | ||
Review via email: mp+190975@code.launchpad.net |
Commit message
Description of the change
The new API
- 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)
-
[MERGE] from trunk
- 5187. By Raphael Collet (OpenERP)
-
[IMP] orm: do not override some magic fields when they are already declared
- 5188. By Raphael Collet (OpenERP)
-
[FIX] orm: in _apply_ir_rules, fix and simplify code that tests truthness of a model
- in the new API, the model is an empty instance, hence not true
- as the 'child_model' is always self, simply use self - 5189. By Raphael Collet (OpenERP)
-
[IMP] yaml_import: add hook to get the right line number when debugging python in yaml files
- 5190. By Raphael Collet (OpenERP)
-
[FIX] ir_actions: wrong test with self.pool.get
- 5191. By Raphael Collet (OpenERP)
-
[IMP] api: in guess(), old-style methods with kwargs are assumed to use context
- 5192. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5193. By Raphael Collet (OpenERP)
-
[FIX] api: make api guesser more robust when class value is not a function
- 5194. By Raphael Collet (OpenERP)
-
[FIX] ir_ui_view: abusive use of registry.get
- 5195. By Raphael Collet (OpenERP)
-
[FIX] ir_model: return None instead of a browse_null, since no model is available
- 5196. By Raphael Collet (OpenERP)
-
[FIX] res_users: rename _table_name into _name
- 5197. By Raphael Collet (OpenERP)
-
[FIX] ir_qweb: fix old-fashioned ._model
- 5198. By Raphael Collet (OpenERP)
-
[FIX] tests: mute logger in ir_ui_view
- 5199. By Raphael Collet (OpenERP)
-
[FIX] orm: use clear() to reset the ormcache dict, instead of assigning ormcache on an instance!
- 5200. By Raphael Collet (OpenERP)
-
[FIX] orm: reintroduce attribute _model for backward compatibility
- 5201. By Raphael Collet (OpenERP)
-
[FIX] fields: reintroduce class method selection.reify()
- 5202. By Raphael Collet (OpenERP)
-
[FIX] fields: parameter 'domain' may be a callable that takes one argument (the model)
- 5203. By Raphael Collet (OpenERP)
-
[FIX] ir_ui_view: add api decorator to method that does not follow conventions
- 5204. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5205. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5206. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5207. By Raphael Collet (OpenERP)
-
[FIX] openerp: fix imports (circular dependencies)
- 5208. By Raphael Collet (OpenERP)
-
[FIX] tests: move test_fields under module test_new_api, which defines a model for it
- 5209. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5210. By Raphael Collet (OpenERP)
-
[IMP] orm: rewrite method browse in both old and new style
- 5211. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
Christophe Simonis (OpenERP) (kangol) wrote : | # |
Christophe Simonis (OpenERP) (kangol) wrote : | # |
About one scope per xml/yaml file: After some tests, it make the code more complex, difficult to read. And runbot turn red, so I must left some place where to create another scope. A more importantly, it doesn't seem to make any difference in term of speed. So keep it as it (at least for now)
- 5212. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5213. By Raphael Collet (OpenERP)
-
[FIX] openerp.http: introduce scope for processing request
- 5214. By Raphael Collet (OpenERP)
-
[IMP] orm: turn recomputation manager's api into a mutable mapping
- 5215. By Raphael Collet (OpenERP)
-
[IMP] orm: use a proxy dictionary to access a record's cache as record._cache[name]
- 5216. By Raphael Collet (OpenERP)
-
[IMP] orm: improve method onchange() to handle *2many fields
- 5217. By Raphael Collet (OpenERP)
-
[FIX] orm: do not set inverse fields in cache, as this does not work with related fields
- 5218. By Raphael Collet (OpenERP)
-
[IMP] orm, fields: improve method convert_to_write() to have nicer results in the case of onchange()
- 5219. By Raphael Collet (OpenERP)
-
[IMP] orm: improve method record.map(fields)
- 5220. By Raphael Collet (OpenERP)
-
[ADD] orm: add flag _dirty on records, and use it to determine changes in onchange()
- 5221. By Raphael Collet (OpenERP)
-
[IMP] orm: move code around, and add some documentation
- 5222. By Raphael Collet (OpenERP)
-
[FIX] orm: small fixes and improvements in RecordCache
- 5223. By Raphael Collet (OpenERP)
-
[IMP] test_new_api: rewrite all models and tests, and define views for manual test
- 5224. By Raphael Collet (OpenERP)
-
[FIX] orm, test_new_api: fix onchange method to handle {one,many}2many fields
- 5225. By Raphael Collet (OpenERP)
-
[IMP] fields2: when assigning records.stuff = value, only records[0] is updated
- 5226. By Raphael Collet (OpenERP)
-
[FIX] orm, test_new_api: fix onchange method in special case (modified {one,many}2many field becomes dirty)
- 5227. By Raphael Collet (OpenERP)
-
[IMP] orm: move hack in onchange() to something cleaner in new()
- 5228. By Raphael Collet (OpenERP)
-
[FIX] fields2: related fields never have an inverse field
- 5229. By Raphael Collet (OpenERP)
-
[IMP] orm: setitem in record._cache does not convert value
- 5230. By Raphael Collet (OpenERP)
-
[IMP] orm: optimize cache invalidation in methods _create() and _write()
- 5231. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5232. By Raphael Collet (OpenERP)
-
[FIX] fields2: properties 'model' and 'comodel' cannot be memoized, since they refer to a scope
- 5233. By Raphael Collet (OpenERP)
-
[IMP] orm: swap arguments of generic method onchange()
- 5234. By Raphael Collet (OpenERP)
-
[FIX] fields2: fix Many2many.
inverse_ field by setting up field attributes correcly - 5235. By Raphael Collet (OpenERP)
-
[FIX] orm: allow method new() to take 'id' in values; this helps onchange()
- 5236. By Raphael Collet (OpenERP)
-
[IMP] orm: in onchange(), for *2many fields, export the subfields in tocheck only
- 5237. By Raphael Collet (OpenERP)
-
[FIX] test_new_api: fix views
- 5238. By Raphael Collet (OpenERP)
-
[IMP] orm: in onchange(), prefetch all subrecords to avoid having false dirty records
- 5239. By Raphael Collet (OpenERP)
-
[ADD] ir.ui.view: automatic addition of on_change attributes using computed field dependencies
- 5240. By Raphael Collet (OpenERP)
-
[IMP] yaml_import: add new-style onchange evaluation
- 5241. By Raphael Collet (OpenERP)
-
[IMP] fields2: allow assigning date strings to datetime fields (automatically add 00:00:00)
- 5242. By Raphael Collet (OpenERP)
-
[FIX] when converting many2one for write, never let a NewId escape, and return a dict instead
- 5243. By Raphael Collet (OpenERP)
-
[IMP] orm: improve assert message in browse()
- 5244. By Raphael Collet (OpenERP)
-
[FIX] orm: improve onchange() to not let it modify its arguments
- 5245. By Raphael Collet (OpenERP)
-
[FIX] fields2: in Many2one.
convert_ to_write, convert dictionary too - 5246. By Raphael Collet (OpenERP)
-
[IMP] test_new_api: add missing access rights
- 5247. By Raphael Collet (OpenERP)
-
[FIX] fields2: in One2many.
convert_ to_write, select fields from the right model! - 5248. By Raphael Collet (OpenERP)
-
[IMP] design change: make the registry instance old-style API, and use new-style API with records only
- 5249. By Raphael Collet (OpenERP)
-
[IMP] orm: rename method scoped() into attach_scope() and improve it
- 5250. By Raphael Collet (OpenERP)
-
[IMP] fields2: improve implementation of get_description(), to_column(), and setup_related()
- 5251. By Raphael Collet (OpenERP)
-
[IMP] fields2: remove properties 'model' and 'comodel' that were depending on scope
- 5252. By Raphael Collet (OpenERP)
-
[IMP] fields2: make get_description() scope-free
- 5253. By Raphael Collet (OpenERP)
-
[IMP] fields2: make methods determine_* and modified_* scope-free
- 5254. By Raphael Collet (OpenERP)
-
[IMP] fields2: make methods convert_to_* scope-free
- 5255. By Raphael Collet (OpenERP)
-
[IMP] fields2: make all methods scope-free
- 5256. By Raphael Collet (OpenERP)
-
[IMP] fields2: simple code reorganization
- 5257. By Raphael Collet (OpenERP)
-
[IMP] scope: reimplement draft mode and recomputation object
- 5258. By Raphael Collet (OpenERP)
-
[IMP] orm: decorate invalidate_cache() with @model, and fix calls accordingly
- 5259. By Raphael Collet (OpenERP)
-
[IMP] api: add function propagate_returns()
- 5260. By Raphael Collet (OpenERP)
-
[IMP] design change: scopes are no longer stacked, we only use the scope of records
- 5261. By Raphael Collet (OpenERP)
-
[FIX] test_new_api: fix scope usage according to new design
- 5262. By Raphael Collet (OpenERP)
-
[IMP] orm: add method sudo() on records
- 5263. By Raphael Collet (OpenERP)
-
[IMP] openerp: remove unused imports
- 5264. By Raphael Collet (OpenERP)
-
[IMP] fields2: improve field triggers (and hide more their implementation)
- 5265. By Raphael Collet (OpenERP)
-
[FIX] scope: method invalidate() was unexpectedly screwing up cache while computing default values
- 5266. By Raphael Collet (OpenERP)
-
[FIX] yaml_import: fix call to onchange()
- 5267. By Raphael Collet (OpenERP)
-
[IMP] orm: improve api of _prefetch_field() and _in_cache_without()
- 5268. By Raphael Collet (OpenERP)
-
[FIX] openerp/
tools/translate : adapt to api changes - 5269. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5270. By Raphael Collet (OpenERP)
-
[IMP] orm: print traceback when recomputation fails
- 5271. By Raphael Collet (OpenERP)
-
[IMP] scope: in scope, store context as an immutable dict
- 5272. By Raphael Collet (OpenERP)
-
[IMP] yaml_import: when logging assertion failures, print line number
- 5273. By Raphael Collet (OpenERP)
-
[FIX] scope: add method has_key() in Context, for backward compatibility
- 5274. By Raphael Collet (OpenERP)
-
[IMP] base: do not modify context dictionary in method
- 5275. By Raphael Collet (OpenERP)
-
[IMP] scope: improve usage of immutable dict, and move its implementation to openerp.tools
- 5276. By Raphael Collet (OpenERP)
-
[FIX] ir_cron: add scope management, and make it reentrant
- 5277. By Raphael Collet (OpenERP)
-
[IMP] scope: remove method sudo() on scopes, which is not much useful, contrary to sudo() on records
- 5278. By Raphael Collet (OpenERP)
-
[IMP] api: fix documentation
- 5279. By Raphael Collet (OpenERP)
-
[REF] scope: rename 'scope' into 'env', and 'Scope' into 'Environment'
- 5280. By Raphael Collet (OpenERP)
-
[IMP] orm: rename method map() into _map_field()
- 5281. By Raphael Collet (OpenERP)
-
[IMP] orm, env: rename property cache_ids into prefetch
- 5282. By Raphael Collet (OpenERP)
-
[IMP] orm, env: simplify and improve implementation of recomputation
- 5283. By Raphael Collet (OpenERP)
-
[IMP] orm, env: improve implementation of draft mode
- 5284. By Raphael Collet (OpenERP)
-
[IMP] fields2: improve management of compute, inverse, search, and add support for default values
- 5285. By Raphael Collet (OpenERP)
-
[FIX] fields2: add support for recursively defined fields
- 5286. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5287. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5288. By Raphael Collet (OpenERP)
-
[IMP] orm: simplify new-style api of field-specific onchange methods
- 5289. By Raphael Collet (OpenERP)
-
[IMP] ir_ui_view: automatically add on_change="1" in the presence of a specific onchange method
- 5290. By Raphael Collet (OpenERP)
-
[IMP] fields2: add methods today(), now(), from_string() and to_string() on Date and Datetime
- 5291. By Raphael Collet (OpenERP)
-
[IMP] orm: add methods filter() and map() on records
- 5292. By Raphael Collet (OpenERP)
-
[IMP] openerp: add _ and new field classes in top-level module
- 5293. By Raphael Collet (OpenERP)
-
[IMP] fields2: allow a function to be passed as default value
- 5294. By Raphael Collet (OpenERP)
-
[IMP] orm: improve method sudo(), and rename attach_env to _attach_env
- 5295. By Raphael Collet (OpenERP)
-
[FIX] ir_ui_menu: rewrite method _filter_
visible_ menu() because of an infinite recursion when prefetching menu.child_id - 5296. By Raphael Collet (OpenERP)
-
[FIX] fields2: handle the case where an integer field is used as inverse for a one2many
- 5297. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5298. By Raphael Collet (OpenERP)
-
[IMP] orm: rename attribute _env to env
- 5299. By Raphael Collet (OpenERP)
-
[FIX] fields2: make field 'id' stored in order to make it present in 'ir.model.fields'
- 5300. By Raphael Collet (OpenERP)
-
[FIX] orm: fix value of 'select_level' when creating entries in 'ir.model.fields'
- 5301. By Raphael Collet (OpenERP)
-
[IMP] fields2: add an option 'index' to create an index
- 5302. By Raphael Collet (OpenERP)
-
[IMP] env: use the new API of ir.model.data
- 5303. By Raphael Collet (OpenERP)
-
[IMP] yaml_import: extend tag !python to support the new api
- 5304. By Raphael Collet (OpenERP)
-
[IMP] orm: in method exists(), mark non-existing records in cache
- 5305. By Raphael Collet (OpenERP)
-
[IMP] orm, fields2: when recomputing a stored field, mark all computed fields as done
- 5306. By Raphael Collet (OpenERP)
-
[IMP] fields2: when recomputing several fields, save them all in one shot
- 5307. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5308. By Raphael Collet (OpenERP)
-
[FIX] fields2: clarify method determine_value, and use self.depends instead of self.compute for computed fields
- 5309. By Raphael Collet (OpenERP)
-
[FIX] fields2: recompute records in their original env
- 5310. By Raphael Collet (OpenERP)
-
[IMP] orm: optimize onchange() by removing prefetching that no longer seems necessary
- 5311. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5312. By Raphael Collet (OpenERP)
-
[IMP] fields, field2: add attributes 'deprecated' and 'oldname' on Field
- 5313. By Raphael Collet (OpenERP)
-
[IMP] api: add an explicit decorator for onchange methods
- 5314. By Raphael Collet (OpenERP)
-
[IMP] orm, fields2: improve api of method add_default_value
- 5315. By Raphael Collet (OpenERP)
-
[FIX] fields2: when assigning a dependency of a field being computed, do not invalidate the latter
- 5316. By Raphael Collet (OpenERP)
-
[IMP] orm: remove property _id, and change method unbrowse() into property ids
- 5317. By Raphael Collet (OpenERP)
-
[IMP] fields2: add support for free attributes on fields; they are passed as parameters when converting to a column object
- 5318. By Raphael Collet (OpenERP)
-
[IMP] fields2: rename interface_for into _origin
- 5319. By Raphael Collet (OpenERP)
-
[FIX] res_config: fixed lookup for non-existing column
- 5320. By Raphael Collet (OpenERP)
-
[REV] orm: reintroduce method __getattr__ for signals because it is used everywhere in addons
- 5321. By Raphael Collet (OpenERP)
-
[MERGE] from trunk
- 5322. By Raphael Collet (OpenERP)
-
[IMP] api: optimize hasattr(self, '_ids') in method wrapper
- 5323. By Raphael Collet (OpenERP)
-
[FIX] fields2: in related fields, do not copy attributes required and states from related field
- 5324. By Raphael Collet (OpenERP)
-
[FIX] fields2: web client does not like (5,) in many2many fields
- 5325. By Raphael Collet (OpenERP)
-
[FIX] ir_ui_view: use _fields instead of _columns or _all_columns, because pure computed fields are not recognized
- 5326. By Raphael Collet (OpenERP)
-
[IMP] res_currency: improve new api of methods like round(), is_zero(), etc
- 5327. By Raphael Collet (OpenERP)
-
[IMP] orm: add small method for sorting recordsets
- 5328. By Raphael Collet (OpenERP)
-
[IMP] env: add parameter to method ref() to prevent exceptions
- 5329. By Raphael Collet (OpenERP)
-
[IMP] fields2: improve convert_to_write for *2many fields
- 5330. By Raphael Collet (OpenERP)
-
[FIX] res_users: when reifying a list of groups, parse the list in case it contains commands
- 5331. By Raphael Collet (OpenERP)
-
[FIX] test_new_api: fix test on copied attributes of related fields
- 5332. By Raphael Collet (OpenERP)
-
[IMP] orm: when updating tables, recompute fields with actual dependencies
- 5333. By Raphael Collet (OpenERP)
-
[FIX] orm, fields2: for recomputing fields, now recompute() saves to database
The former implementation had a subtle bug when a field to recompute depends on
another field to recompute. The second field was determined in draft mode, so it
was computed in cache but not saved to database. Later, method recompute() finds
its value in cache and marks it as done! - 5334. By Raphael Collet (OpenERP)
-
[FIX] orm: in onchange(), do not expect field to be in values; this happens in yaml files when field is false
- 5335. By Raphael Collet (OpenERP)
-
[ADD] fields2: method Date.context_
today()
Unmerged revisions
- 5335. By Raphael Collet (OpenERP)
-
[ADD] fields2: method Date.context_
today() - 5334. By Raphael Collet (OpenERP)
-
[FIX] orm: in onchange(), do not expect field to be in values; this happens in yaml files when field is false
- 5333. By Raphael Collet (OpenERP)
-
[FIX] orm, fields2: for recomputing fields, now recompute() saves to database
The former implementation had a subtle bug when a field to recompute depends on
another field to recompute. The second field was determined in draft mode, so it
was computed in cache but not saved to database. Later, method recompute() finds
its value in cache and marks it as done! - 5332. By Raphael Collet (OpenERP)
-
[IMP] orm: when updating tables, recompute fields with actual dependencies
- 5331. By Raphael Collet (OpenERP)
-
[FIX] test_new_api: fix test on copied attributes of related fields
- 5330. By Raphael Collet (OpenERP)
-
[FIX] res_users: when reifying a list of groups, parse the list in case it contains commands
- 5329. By Raphael Collet (OpenERP)
-
[IMP] fields2: improve convert_to_write for *2many fields
- 5328. By Raphael Collet (OpenERP)
-
[IMP] env: add parameter to method ref() to prevent exceptions
- 5327. By Raphael Collet (OpenERP)
-
[IMP] orm: add small method for sorting recordsets
- 5326. By Raphael Collet (OpenERP)
-
[IMP] res_currency: improve new api of methods like round(), is_zero(), etc
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-05-19 13:53:27 +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-05-19 13:53:27 +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-05-19 13:53:27 +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-05-19 13:53:27 +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-05-19 13:53:27 +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 2014-02-09 15:18:22 +0000 |
245 | +++ openerp/__init__.py 2014-05-19 13:53:27 +0000 |
246 | @@ -67,9 +67,7 @@ |
247 | # Imports |
248 | #---------------------------------------------------------- |
249 | import addons |
250 | -import cli |
251 | import conf |
252 | -import http |
253 | import loglevels |
254 | import modules |
255 | import netsvc |
256 | @@ -82,5 +80,22 @@ |
257 | import tools |
258 | import workflow |
259 | |
260 | +#---------------------------------------------------------- |
261 | +# Model classes, fields, api decorators, and environment |
262 | +#---------------------------------------------------------- |
263 | +from openerp.osv.orm import BaseModel, AbstractModel, Model, TransientModel |
264 | +from openerp.osv import fields2 as fields |
265 | +from openerp.osv.fields2 import Boolean, Integer, Float, Char, Text, Html, \ |
266 | + Date, Datetime, Binary, Selection, Reference, Many2one, One2many, Many2many |
267 | +from openerp.osv import api |
268 | +from openerp.osv.api import model, multi, one, constrains, depends, onchange, returns |
269 | +from openerp.osv.env import Environment |
270 | +from openerp.tools.translate import _ |
271 | + |
272 | +#---------------------------------------------------------- |
273 | +# Other imports, which may require stuff from above |
274 | +#---------------------------------------------------------- |
275 | +import cli |
276 | +import http |
277 | + |
278 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
279 | - |
280 | |
281 | === modified file 'openerp/addons/base/__openerp__.py' |
282 | --- openerp/addons/base/__openerp__.py 2014-05-01 18:42:17 +0000 |
283 | +++ openerp/addons/base/__openerp__.py 2014-05-19 13:53:27 +0000 |
284 | @@ -39,7 +39,6 @@ |
285 | 'res/res_country_data.xml', |
286 | 'security/base_security.xml', |
287 | 'base_menu.xml', |
288 | - 'res/res_security.xml', |
289 | 'res/res_config.xml', |
290 | 'res/res.country.state.csv', |
291 | 'ir/ir_actions.xml', |
292 | @@ -83,7 +82,7 @@ |
293 | 'res/res_users_view.xml', |
294 | 'res/res_partner_data.xml', |
295 | 'res/ir_property_view.xml', |
296 | - 'security/base_security.xml', |
297 | + 'res/res_security.xml', |
298 | 'security/ir.model.access.csv', |
299 | ], |
300 | 'demo': [ |
301 | |
302 | === modified file 'openerp/addons/base/base_menu.xml' |
303 | --- openerp/addons/base/base_menu.xml 2013-10-06 11:26:08 +0000 |
304 | +++ openerp/addons/base/base_menu.xml 2014-05-19 13:53:27 +0000 |
305 | @@ -28,6 +28,10 @@ |
306 | <menuitem id="menu_security" name="Security" parent="menu_custom" sequence="25"/> |
307 | <menuitem id="menu_ir_property" name="Parameters" parent="menu_custom" sequence="24"/> |
308 | |
309 | + <record model="ir.ui.menu" id="base.menu_administration"> |
310 | + <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/> |
311 | + </record> |
312 | + |
313 | <record id="action_client_base_menu" model="ir.actions.client"> |
314 | <field name="name">Open Settings Menu</field> |
315 | <field name="tag">reload</field> |
316 | |
317 | === modified file 'openerp/addons/base/ir/ir_actions.py' |
318 | --- openerp/addons/base/ir/ir_actions.py 2014-05-09 09:49:20 +0000 |
319 | +++ openerp/addons/base/ir/ir_actions.py 2014-05-19 13:53:27 +0000 |
320 | @@ -316,14 +316,14 @@ |
321 | } |
322 | for res in results: |
323 | model = res.get('res_model') |
324 | - if model and self.pool.get(model): |
325 | + if model in self.pool: |
326 | try: |
327 | with tools.mute_logger("openerp.tools.safe_eval"): |
328 | eval_context = eval(res['context'] or "{}", eval_dict) or {} |
329 | except Exception: |
330 | continue |
331 | custom_context = dict(context, **eval_context) |
332 | - res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context) |
333 | + res['help'] = self.pool[model].get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context) |
334 | if ids_int: |
335 | return results[0] |
336 | return results |
337 | @@ -339,7 +339,7 @@ |
338 | dataobj = self.pool.get('ir.model.data') |
339 | data_id = dataobj._get_id (cr, SUPERUSER_ID, module, xml_id) |
340 | res_id = dataobj.browse(cr, uid, data_id, context).res_id |
341 | - return self.read(cr, uid, res_id, [], context) |
342 | + return self.read(cr, uid, [res_id], [], context)[0] |
343 | |
344 | VIEW_TYPES = [ |
345 | ('tree', 'Tree'), |
346 | @@ -559,7 +559,7 @@ |
347 | 'sequence': 5, |
348 | 'code': """# You can use the following variables: |
349 | # - self: ORM model of the record on which the action is triggered |
350 | -# - object: browse_record of the record on which the action is triggered if there is one, otherwise None |
351 | +# - object: Record on which the action is triggered if there is one, otherwise None |
352 | # - pool: ORM model pool (i.e. self.pool) |
353 | # - cr: database cursor |
354 | # - uid: current user id |
355 | @@ -815,7 +815,7 @@ |
356 | def run_action_client_action(self, cr, uid, action, eval_context=None, context=None): |
357 | if not action.action_id: |
358 | raise osv.except_osv(_('Error'), _("Please specify an action to launch!")) |
359 | - return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context) |
360 | + return self.pool[action.action_id.type].read(cr, uid, [action.action_id.id], context=context)[0] |
361 | |
362 | def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None): |
363 | eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action' |
364 | @@ -1077,10 +1077,10 @@ |
365 | wizard.write({'state': 'done'}) |
366 | |
367 | # Load action |
368 | - act_type = self.pool.get('ir.actions.actions').read(cr, uid, wizard.action_id.id, ['type'], context=context) |
369 | + act_type = wizard.action_id.type |
370 | |
371 | - res = self.pool[act_type['type']].read(cr, uid, wizard.action_id.id, [], context=context) |
372 | - if act_type['type'] != 'ir.actions.act_window': |
373 | + res = self.pool[act_type].read(cr, uid, [wizard.action_id.id], [], context=context)[0] |
374 | + if act_type != 'ir.actions.act_window': |
375 | return res |
376 | res.setdefault('context','{}') |
377 | res['nodestroy'] = True |
378 | |
379 | === modified file 'openerp/addons/base/ir/ir_attachment.py' |
380 | --- openerp/addons/base/ir/ir_attachment.py 2014-04-10 15:20:39 +0000 |
381 | +++ openerp/addons/base/ir/ir_attachment.py 2014-05-19 13:53:27 +0000 |
382 | @@ -273,7 +273,7 @@ |
383 | # performed in batch as much as possible. |
384 | ima = self.pool.get('ir.model.access') |
385 | for model, targets in model_attachments.iteritems(): |
386 | - if not self.pool.get(model): |
387 | + if model not in self.pool: |
388 | continue |
389 | if not ima.check(cr, uid, model, 'read', False): |
390 | # remove all corresponding attachment ids |
391 | @@ -297,7 +297,7 @@ |
392 | if isinstance(ids, (int, long)): |
393 | ids = [ids] |
394 | self.check(cr, uid, ids, 'read', context=context) |
395 | - return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context, load) |
396 | + return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context=context, load=load) |
397 | |
398 | def write(self, cr, uid, ids, vals, context=None): |
399 | if isinstance(ids, (int, long)): |
400 | |
401 | === modified file 'openerp/addons/base/ir/ir_cron.py' |
402 | --- openerp/addons/base/ir/ir_cron.py 2014-02-28 16:15:24 +0000 |
403 | +++ openerp/addons/base/ir/ir_cron.py 2014-05-19 13:53:27 +0000 |
404 | @@ -26,7 +26,7 @@ |
405 | from dateutil.relativedelta import relativedelta |
406 | |
407 | import openerp |
408 | -from openerp import netsvc |
409 | +from openerp import SUPERUSER_ID, netsvc, Environment |
410 | from openerp.osv import fields, osv |
411 | from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT |
412 | from openerp.tools.safe_eval import safe_eval as eval |
413 | @@ -149,36 +149,38 @@ |
414 | except Exception, e: |
415 | self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e) |
416 | |
417 | - def _process_job(self, job_cr, job, cron_cr): |
418 | + def _process_job(self, cr, job, cron_cr): |
419 | """ Run a given job taking care of the repetition. |
420 | |
421 | - :param job_cr: cursor to use to execute the job, safe to commit/rollback |
422 | + :param cr: cursor to use to execute the job, safe to commit/rollback |
423 | :param job: job to be run (as a dictionary). |
424 | :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date, |
425 | must not be committed/rolled back! |
426 | """ |
427 | try: |
428 | - now = datetime.now() |
429 | - nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT) |
430 | - numbercall = job['numbercall'] |
431 | + with Environment.manage(): |
432 | + now = datetime.now() |
433 | + nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT) |
434 | + numbercall = job['numbercall'] |
435 | |
436 | - ok = False |
437 | - while nextcall < now and numbercall: |
438 | - if numbercall > 0: |
439 | - numbercall -= 1 |
440 | - if not ok or job['doall']: |
441 | - self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id']) |
442 | - if numbercall: |
443 | - nextcall += _intervalTypes[job['interval_type']](job['interval_number']) |
444 | - ok = True |
445 | - addsql = '' |
446 | - if not numbercall: |
447 | - addsql = ', active=False' |
448 | - cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s", |
449 | - (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id'])) |
450 | + ok = False |
451 | + while nextcall < now and numbercall: |
452 | + if numbercall > 0: |
453 | + numbercall -= 1 |
454 | + if not ok or job['doall']: |
455 | + self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id']) |
456 | + if numbercall: |
457 | + nextcall += _intervalTypes[job['interval_type']](job['interval_number']) |
458 | + ok = True |
459 | + addsql = '' |
460 | + if not numbercall: |
461 | + addsql = ', active=False' |
462 | + cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s", |
463 | + (nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id'])) |
464 | + self.invalidate_cache(cr, SUPERUSER_ID) |
465 | |
466 | finally: |
467 | - job_cr.commit() |
468 | + cr.commit() |
469 | cron_cr.commit() |
470 | |
471 | @classmethod |
472 | |
473 | === modified file 'openerp/addons/base/ir/ir_mail_server.py' |
474 | --- openerp/addons/base/ir/ir_mail_server.py 2013-11-23 11:30:53 +0000 |
475 | +++ openerp/addons/base/ir/ir_mail_server.py 2014-05-19 13:53:27 +0000 |
476 | @@ -461,21 +461,18 @@ |
477 | mdir.add(message.as_string(True)) |
478 | return message_id |
479 | |
480 | + smtp = None |
481 | try: |
482 | smtp = self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption or False, smtp_debug) |
483 | smtp.sendmail(smtp_from, smtp_to_list, message.as_string()) |
484 | finally: |
485 | - try: |
486 | - # Close Connection of SMTP Server |
487 | + if smtp is not None: |
488 | smtp.quit() |
489 | - except Exception: |
490 | - # ignored, just a consequence of the previous exception |
491 | - pass |
492 | except Exception, e: |
493 | msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (tools.ustr(smtp_server), |
494 | e.__class__.__name__, |
495 | tools.ustr(e)) |
496 | - _logger.exception(msg) |
497 | + _logger.error(msg) |
498 | raise MailDeliveryException(_("Mail Delivery Failed"), msg) |
499 | return message_id |
500 | |
501 | |
502 | === modified file 'openerp/addons/base/ir/ir_model.py' |
503 | --- openerp/addons/base/ir/ir_model.py 2014-05-06 12:16:27 +0000 |
504 | +++ openerp/addons/base/ir/ir_model.py 2014-05-19 13:53:27 +0000 |
505 | @@ -28,12 +28,11 @@ |
506 | import openerp.modules.registry |
507 | from openerp import SUPERUSER_ID |
508 | from openerp import tools |
509 | -from openerp.osv import fields,osv |
510 | -from openerp.osv.orm import Model, browse_null |
511 | +from openerp.osv import fields, osv |
512 | +from openerp.osv.orm import BaseModel, Model, MAGIC_COLUMNS, except_orm |
513 | +from openerp.tools import config |
514 | from openerp.tools.safe_eval import safe_eval as eval |
515 | -from openerp.tools import config |
516 | from openerp.tools.translate import _ |
517 | -from openerp.osv.orm import except_orm, browse_record, MAGIC_COLUMNS |
518 | |
519 | _logger = logging.getLogger(__name__) |
520 | |
521 | @@ -133,15 +132,10 @@ |
522 | ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'), |
523 | ] |
524 | |
525 | - # overridden to allow searching both on model name (model field) |
526 | - # and model description (name field) |
527 | - def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None): |
528 | - if args is None: |
529 | - args = [] |
530 | - domain = args + ['|', ('model', operator, name), ('name', operator, name)] |
531 | - return self.name_get(cr, name_get_uid or uid, |
532 | - super(ir_model, self).search(cr, uid, domain, limit=limit, context=context), |
533 | - context=context) |
534 | + def _search_display_name(self, operator, value): |
535 | + # overridden to allow searching both on model name (model field) and |
536 | + # model description (name field) |
537 | + return ['|', ('model', operator, value), ('name', operator, value)] |
538 | |
539 | def _drop_table(self, cr, uid, ids, context=None): |
540 | for model in self.browse(cr, uid, ids, context): |
541 | @@ -177,6 +171,7 @@ |
542 | |
543 | def write(self, cr, user, ids, vals, context=None): |
544 | if context: |
545 | + context = dict(context) |
546 | context.pop('__last_update', None) |
547 | # Filter out operations 4 link from field id, because openerp-web |
548 | # always write (4,id,False) even for non dirty items |
549 | @@ -207,7 +202,7 @@ |
550 | _custom = True |
551 | x_custom_model._name = model |
552 | x_custom_model._module = False |
553 | - a = x_custom_model.create_instance(self.pool, cr) |
554 | + a = x_custom_model._build_model(self.pool, cr) |
555 | if not a._columns: |
556 | x_name = 'id' |
557 | elif 'x_name' in a._columns.keys(): |
558 | @@ -627,8 +622,8 @@ |
559 | """ Check if a specific group has the access mode to the specified model""" |
560 | assert mode in ['read','write','create','unlink'], 'Invalid access mode' |
561 | |
562 | - if isinstance(model, browse_record): |
563 | - assert model._table_name == 'ir.model', 'Invalid model object' |
564 | + if isinstance(model, BaseModel): |
565 | + assert model._name == 'ir.model', 'Invalid model object' |
566 | model_name = model.name |
567 | else: |
568 | model_name = model |
569 | @@ -686,8 +681,8 @@ |
570 | |
571 | assert mode in ['read','write','create','unlink'], 'Invalid access mode' |
572 | |
573 | - if isinstance(model, browse_record): |
574 | - assert model._table_name == 'ir.model', 'Invalid model object' |
575 | + if isinstance(model, BaseModel): |
576 | + assert model._name == 'ir.model', 'Invalid model object' |
577 | model_name = model.model |
578 | else: |
579 | model_name = model |
580 | @@ -755,6 +750,7 @@ |
581 | pass |
582 | |
583 | def call_cache_clearing_methods(self, cr): |
584 | + self.invalidate_cache(cr, SUPERUSER_ID) |
585 | self.check.clear_cache(self) # clear the cache of check function |
586 | for model, method in self.__cache_clearing_methods: |
587 | if model in self.pool: |
588 | @@ -763,19 +759,19 @@ |
589 | # |
590 | # Check rights on actions |
591 | # |
592 | - def write(self, cr, uid, *args, **argv): |
593 | - self.call_cache_clearing_methods(cr) |
594 | - res = super(ir_model_access, self).write(cr, uid, *args, **argv) |
595 | - return res |
596 | - |
597 | - def create(self, cr, uid, *args, **argv): |
598 | - self.call_cache_clearing_methods(cr) |
599 | - res = super(ir_model_access, self).create(cr, uid, *args, **argv) |
600 | - return res |
601 | - |
602 | - def unlink(self, cr, uid, *args, **argv): |
603 | - self.call_cache_clearing_methods(cr) |
604 | - res = super(ir_model_access, self).unlink(cr, uid, *args, **argv) |
605 | + def write(self, cr, uid, ids, values, context=None): |
606 | + self.call_cache_clearing_methods(cr) |
607 | + res = super(ir_model_access, self).write(cr, uid, ids, values, context=context) |
608 | + return res |
609 | + |
610 | + def create(self, cr, uid, values, context=None): |
611 | + self.call_cache_clearing_methods(cr) |
612 | + res = super(ir_model_access, self).create(cr, uid, values, context=context) |
613 | + return res |
614 | + |
615 | + def unlink(self, cr, uid, ids, context=None): |
616 | + self.call_cache_clearing_methods(cr) |
617 | + res = super(ir_model_access, self).unlink(cr, uid, ids, context=context) |
618 | return res |
619 | |
620 | class ir_model_data(osv.osv): |
621 | @@ -831,8 +827,8 @@ |
622 | 'date_init': fields.datetime('Init Date') |
623 | } |
624 | _defaults = { |
625 | - 'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), |
626 | - 'date_update': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), |
627 | + 'date_init': fields.datetime.now, |
628 | + 'date_update': fields.datetime.now, |
629 | 'noupdate': False, |
630 | 'module': '' |
631 | } |
632 | @@ -842,12 +838,11 @@ |
633 | |
634 | def __init__(self, pool, cr): |
635 | osv.osv.__init__(self, pool, cr) |
636 | - self.doinit = True |
637 | # also stored in pool to avoid being discarded along with this osv instance |
638 | if getattr(pool, 'model_data_reference_ids', None) is None: |
639 | self.pool.model_data_reference_ids = {} |
640 | - |
641 | - self.loads = self.pool.model_data_reference_ids |
642 | + # put loads on the class, in order to share it among all instances |
643 | + type(self).loads = self.pool.model_data_reference_ids |
644 | |
645 | def _auto_init(self, cr, context=None): |
646 | super(ir_model_data, self)._auto_init(cr, context) |
647 | @@ -886,7 +881,7 @@ |
648 | |
649 | def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None): |
650 | """ Return a browse_record |
651 | - if not found and raise_if_not_found is True return the browse_null |
652 | + if not found and raise_if_not_found is True return None |
653 | """ |
654 | t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found) |
655 | res_model, res_id = t |
656 | @@ -897,7 +892,7 @@ |
657 | return record |
658 | if raise_if_not_found: |
659 | raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xml_id)) |
660 | - return browse_null() |
661 | + return None |
662 | |
663 | # OLD API |
664 | def _get_id(self, cr, uid, module, xml_id): |
665 | @@ -922,7 +917,7 @@ |
666 | |
667 | def get_object(self, cr, uid, module, xml_id, context=None): |
668 | """ Returns a browsable record for the given module name and xml_id. |
669 | - If not found, raise a ValueError or return a browse_null, depending |
670 | + If not found, raise a ValueError or return None, depending |
671 | on the value of `raise_exception`. |
672 | """ |
673 | return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context) |
674 | @@ -959,8 +954,6 @@ |
675 | if xml_id and ('.' in xml_id): |
676 | 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 |
677 | module, xml_id = xml_id.split('.') |
678 | - if (not xml_id) and (not self.doinit): |
679 | - return False |
680 | action_id = False |
681 | if xml_id: |
682 | cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate |
683 | @@ -1032,8 +1025,8 @@ |
684 | if xml_id and res_id: |
685 | self.loads[(module, xml_id)] = (model, res_id) |
686 | for table, inherit_field in model_obj._inherits.iteritems(): |
687 | - inherit_id = model_obj.read(cr, uid, res_id, |
688 | - [inherit_field])[inherit_field] |
689 | + inherit_id = model_obj.read(cr, uid, [res_id], |
690 | + [inherit_field])[0][inherit_field] |
691 | self.loads[(module, xml_id + '_' + table.replace('.', '_'))] = (table, inherit_id) |
692 | return res_id |
693 | |
694 | @@ -1056,11 +1049,12 @@ |
695 | |
696 | cr.execute('select * from ir_values where model=%s and key=%s and name=%s'+where,(model, key, name)) |
697 | res = cr.fetchone() |
698 | + ir_values_obj = openerp.registry(cr.dbname)['ir.values'] |
699 | if not res: |
700 | - ir_values_obj = openerp.registry(cr.dbname)['ir.values'] |
701 | ir_values_obj.set(cr, uid, key, key2, name, models, value, replace, isobject, meta) |
702 | elif xml_id: |
703 | cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name)) |
704 | + ir_values_obj.invalidate_cache(cr, uid, ['value']) |
705 | return True |
706 | |
707 | def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None): |
708 | @@ -1102,6 +1096,7 @@ |
709 | 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,)) |
710 | wkf_todo.extend(cr.fetchall()) |
711 | 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)) |
712 | + self.invalidate_cache(cr, uid, context=context) |
713 | |
714 | for model,res_id in wkf_todo: |
715 | try: |
716 | |
717 | === modified file 'openerp/addons/base/ir/ir_qweb.py' |
718 | --- openerp/addons/base/ir/ir_qweb.py 2014-05-08 11:51:57 +0000 |
719 | +++ openerp/addons/base/ir/ir_qweb.py 2014-05-19 13:53:27 +0000 |
720 | @@ -443,7 +443,7 @@ |
721 | record, field_name = template_attributes["field"].rsplit('.', 1) |
722 | record = self.eval_object(record, qwebcontext) |
723 | |
724 | - column = record._model._all_columns[field_name].column |
725 | + column = record._all_columns[field_name].column |
726 | options = json.loads(template_attributes.get('field-options') or '{}') |
727 | field_type = get_field_type(column, options) |
728 | |
729 | @@ -504,10 +504,10 @@ |
730 | |
731 | :returns: iterable of (attribute name, attribute value) pairs. |
732 | """ |
733 | - column = record._model._all_columns[field_name].column |
734 | + column = record._all_columns[field_name].column |
735 | field_type = get_field_type(column, options) |
736 | return [ |
737 | - ('data-oe-model', record._model._name), |
738 | + ('data-oe-model', record._name), |
739 | ('data-oe-id', record.id), |
740 | ('data-oe-field', field_name), |
741 | ('data-oe-type', field_type), |
742 | @@ -539,7 +539,7 @@ |
743 | try: |
744 | content = self.record_to_html( |
745 | cr, uid, field_name, record, |
746 | - record._model._all_columns[field_name].column, |
747 | + record._all_columns[field_name].column, |
748 | options, context=context) |
749 | if options.get('html-escape', True): |
750 | content = werkzeug.utils.escape(content) |
751 | @@ -547,7 +547,7 @@ |
752 | content = content.__html__() |
753 | except Exception: |
754 | _logger.warning("Could not get field %s for model %s", |
755 | - field_name, record._model._name, exc_info=True) |
756 | + field_name, record._name, exc_info=True) |
757 | content = None |
758 | |
759 | if context and context.get('inherit_branding'): |
760 | |
761 | === modified file 'openerp/addons/base/ir/ir_rule.py' |
762 | --- openerp/addons/base/ir/ir_rule.py 2014-05-01 18:42:17 +0000 |
763 | +++ openerp/addons/base/ir/ir_rule.py 2014-05-19 13:53:27 +0000 |
764 | @@ -78,7 +78,7 @@ |
765 | '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"), |
766 | 'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'), |
767 | 'domain_force': fields.text('Domain'), |
768 | - 'domain': fields.function(_domain_force_get, string='Domain', type='text'), |
769 | + 'domain': fields.function(_domain_force_get, string='Domain', type='binary'), |
770 | 'perm_read': fields.boolean('Apply for Read'), |
771 | 'perm_write': fields.boolean('Apply for Write'), |
772 | 'perm_create': fields.boolean('Apply for Create'), |
773 | @@ -127,7 +127,7 @@ |
774 | group_domains = {} # map: group -> list of domains |
775 | for rule in self.browse(cr, SUPERUSER_ID, rule_ids): |
776 | # read 'domain' as UID to have the correct eval context for the rule. |
777 | - rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain'] |
778 | + rule_domain = self.read(cr, uid, [rule.id], ['domain'])[0]['domain'] |
779 | dom = expression.normalize_domain(rule_domain) |
780 | for group in rule.groups: |
781 | if group in user.groups_id: |
782 | |
783 | === modified file 'openerp/addons/base/ir/ir_sequence.py' |
784 | --- openerp/addons/base/ir/ir_sequence.py 2013-08-23 09:56:35 +0000 |
785 | +++ openerp/addons/base/ir/ir_sequence.py 2014-05-19 13:53:27 +0000 |
786 | @@ -234,15 +234,15 @@ |
787 | 'sec': time.strftime('%S', t), |
788 | } |
789 | |
790 | - def _next(self, cr, uid, seq_ids, context=None): |
791 | - if not seq_ids: |
792 | + def _next(self, cr, uid, ids, context=None): |
793 | + if not ids: |
794 | return False |
795 | if context is None: |
796 | context = {} |
797 | force_company = context.get('force_company') |
798 | if not force_company: |
799 | force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id |
800 | - sequences = self.read(cr, uid, seq_ids, ['name','company_id','implementation','number_next','prefix','suffix','padding']) |
801 | + sequences = self.read(cr, uid, ids, ['name','company_id','implementation','number_next','prefix','suffix','padding']) |
802 | preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ] |
803 | seq = preferred_sequences[0] if preferred_sequences else sequences[0] |
804 | if seq['implementation'] == 'standard': |
805 | @@ -251,6 +251,7 @@ |
806 | else: |
807 | cr.execute("SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT", (seq['id'],)) |
808 | cr.execute("UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s ", (seq['id'],)) |
809 | + self.invalidate_cache(cr, uid, ['number_next'], [seq['id']], context=context) |
810 | d = self._interpolation_dict() |
811 | try: |
812 | interpolated_prefix = self._interpolate(seq['prefix'], d) |
813 | |
814 | === modified file 'openerp/addons/base/ir/ir_translation.py' |
815 | --- openerp/addons/base/ir/ir_translation.py 2014-01-22 15:13:27 +0000 |
816 | +++ openerp/addons/base/ir/ir_translation.py 2014-05-19 13:53:27 +0000 |
817 | @@ -168,11 +168,11 @@ |
818 | else: |
819 | model_name, field = record.name.split(',') |
820 | model = self.pool.get(model_name) |
821 | - if model and model.exists(cr, uid, record.res_id, context=context): |
822 | + if model is not None: |
823 | # Pass context without lang, need to read real stored field, not translation |
824 | context_no_lang = dict(context, lang=None) |
825 | - result = model.read(cr, uid, record.res_id, [field], context=context_no_lang) |
826 | - res[record.id] = result[field] if result else False |
827 | + result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang) |
828 | + res[record.id] = result[0][field] if result else False |
829 | return res |
830 | |
831 | def _set_src(self, cr, uid, id, name, value, args, context=None): |
832 | |
833 | === modified file 'openerp/addons/base/ir/ir_ui_menu.py' |
834 | --- openerp/addons/base/ir/ir_ui_menu.py 2013-10-06 13:24:24 +0000 |
835 | +++ openerp/addons/base/ir/ir_ui_menu.py 2014-05-19 13:53:27 +0000 |
836 | @@ -28,7 +28,7 @@ |
837 | import openerp.modules |
838 | from openerp.osv import fields, osv |
839 | from openerp.tools.translate import _ |
840 | -from openerp import SUPERUSER_ID |
841 | +from openerp import SUPERUSER_ID, multi, returns |
842 | |
843 | MENU_ITEM_SEPARATOR = "/" |
844 | |
845 | @@ -36,71 +36,74 @@ |
846 | _name = 'ir.ui.menu' |
847 | |
848 | def __init__(self, *args, **kwargs): |
849 | - self.cache_lock = threading.RLock() |
850 | - self._cache = {} |
851 | + cls = type(self) |
852 | + # by design, self._menu_cache is specific to the database |
853 | + cls._menu_cache_lock = threading.RLock() |
854 | + cls._menu_cache = {} |
855 | super(ir_ui_menu, self).__init__(*args, **kwargs) |
856 | self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache') |
857 | |
858 | def clear_cache(self): |
859 | - with self.cache_lock: |
860 | + with self._menu_cache_lock: |
861 | # radical but this doesn't frequently happen |
862 | - if self._cache: |
863 | + if self._menu_cache: |
864 | # Normally this is done by openerp.tools.ormcache |
865 | # but since we do not use it, set it by ourself. |
866 | self.pool._any_cache_cleared = True |
867 | - self._cache = {} |
868 | + self._menu_cache.clear() |
869 | |
870 | - def _filter_visible_menus(self, cr, uid, ids, context=None): |
871 | - """Filters the give menu ids to only keep the menu items that should be |
872 | - visible in the menu hierarchy of the current user. |
873 | - Uses a cache for speeding up the computation. |
874 | + @multi |
875 | + @returns('self') |
876 | + def _filter_visible_menus(self): |
877 | + """ Filter `self` to only keep the menu items that should be visible in |
878 | + the menu hierarchy of the current user. |
879 | + Uses a cache for speeding up the computation. |
880 | """ |
881 | - with self.cache_lock: |
882 | - modelaccess = self.pool.get('ir.model.access') |
883 | - user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id']) |
884 | - result = [] |
885 | - for menu in self.browse(cr, uid, ids, context=context): |
886 | - # this key works because user access rights are all based on user's groups (cfr ir_model_access.check) |
887 | - key = (cr.dbname, menu.id, tuple(user_groups)) |
888 | - if key in self._cache: |
889 | - if self._cache[key]: |
890 | - result.append(menu.id) |
891 | - #elif not menu.groups_id and not menu.action: |
892 | - # result.append(menu.id) |
893 | - continue |
894 | - |
895 | - self._cache[key] = False |
896 | - if menu.groups_id: |
897 | - restrict_to_groups = [g.id for g in menu.groups_id] |
898 | - if not user_groups.intersection(restrict_to_groups): |
899 | - continue |
900 | - #result.append(menu.id) |
901 | - #self._cache[key] = True |
902 | - #continue |
903 | - |
904 | - if menu.action: |
905 | - # we check if the user has access to the action of the menu |
906 | - data = menu.action |
907 | - if data: |
908 | - model_field = { 'ir.actions.act_window': 'res_model', |
909 | - 'ir.actions.report.xml': 'model', |
910 | - 'ir.actions.wizard': 'model', |
911 | - 'ir.actions.server': 'model_id', |
912 | - } |
913 | - |
914 | - field = model_field.get(menu.action._name) |
915 | - if field and data[field]: |
916 | - if not modelaccess.check(cr, uid, data[field], 'read', False): |
917 | - continue |
918 | - else: |
919 | - # if there is no action, it's a 'folder' menu |
920 | - if not menu.child_id: |
921 | - # not displayed if there is no children |
922 | - continue |
923 | - |
924 | - result.append(menu.id) |
925 | - self._cache[key] = True |
926 | - return result |
927 | + with self._menu_cache_lock: |
928 | + groups = self.env.user.groups_id |
929 | + |
930 | + # visibility is entirely based on the user's groups; |
931 | + # self._menu_cache[key] gives the ids of all visible menus |
932 | + key = frozenset(groups._ids) |
933 | + if key in self._menu_cache: |
934 | + visible = self.browse(self._menu_cache[key]) |
935 | + |
936 | + else: |
937 | + # retrieve all menus, and determine which ones are visible |
938 | + context = {'ir.ui.menu.full_list': True} |
939 | + menus = self.sudo(context=context).search([]) |
940 | + |
941 | + # first discard all menus with groups the user does not have |
942 | + menus = menus.filter( |
943 | + lambda menu: not menu.groups_id or menu.groups_id & groups) |
944 | + |
945 | + # take apart menus that have an action |
946 | + action_menus = menus.filter('action') |
947 | + folder_menus = menus - action_menus |
948 | + visible = self.browse() |
949 | + |
950 | + # process action menus, check whether their action is allowed |
951 | + access = self.env['ir.model.access'] |
952 | + model_fname = { |
953 | + 'ir.actions.act_window': 'res_model', |
954 | + 'ir.actions.report.xml': 'model', |
955 | + 'ir.actions.wizard': 'model', |
956 | + 'ir.actions.server': 'model_id', |
957 | + } |
958 | + for menu in action_menus: |
959 | + fname = model_fname.get(menu.action._name) |
960 | + if not fname or not menu.action[fname] or \ |
961 | + access.check(menu.action[fname], 'read', False): |
962 | + # make menu visible, and its folder ancestors, too |
963 | + visible += menu |
964 | + menu = menu.parent_id |
965 | + while menu and menu in folder_menus and menu not in visible: |
966 | + visible += menu |
967 | + menu = menu.parent_id |
968 | + |
969 | + self._menu_cache[key] = visible._ids |
970 | + |
971 | + return self.filter(lambda menu: menu in visible) |
972 | |
973 | def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): |
974 | if context is None: |
975 | @@ -153,13 +156,13 @@ |
976 | parent_path = '' |
977 | return parent_path + elmt.name |
978 | |
979 | - def create(self, *args, **kwargs): |
980 | + def create(self, cr, uid, values, context=None): |
981 | self.clear_cache() |
982 | - return super(ir_ui_menu, self).create(*args, **kwargs) |
983 | + return super(ir_ui_menu, self).create(cr, uid, values, context=context) |
984 | |
985 | - def write(self, *args, **kwargs): |
986 | + def write(self, cr, uid, ids, values, context=None): |
987 | self.clear_cache() |
988 | - return super(ir_ui_menu, self).write(*args, **kwargs) |
989 | + return super(ir_ui_menu, self).write(cr, uid, ids, values, context=context) |
990 | |
991 | def unlink(self, cr, uid, ids, context=None): |
992 | # Detach children and promote them to top-level, because it would be unwise to |
993 | |
994 | === modified file 'openerp/addons/base/ir/ir_ui_view.py' |
995 | --- openerp/addons/base/ir/ir_ui_view.py 2014-05-12 08:05:23 +0000 |
996 | +++ openerp/addons/base/ir/ir_ui_view.py 2014-05-19 13:53:27 +0000 |
997 | @@ -37,7 +37,7 @@ |
998 | import openerp |
999 | from openerp import tools |
1000 | from openerp.http import request |
1001 | -from openerp.osv import fields, osv, orm |
1002 | +from openerp.osv import fields, osv, orm, api |
1003 | from openerp.tools import graph, SKIPPED_ELEMENT_TYPES |
1004 | from openerp.tools.safe_eval import safe_eval as eval |
1005 | from openerp.tools.view_validation import valid_view |
1006 | @@ -506,7 +506,7 @@ |
1007 | |
1008 | modifiers = {} |
1009 | Model = self.pool.get(model) |
1010 | - if not Model: |
1011 | + if Model is None: |
1012 | self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), |
1013 | view_id, context) |
1014 | |
1015 | @@ -525,10 +525,10 @@ |
1016 | |
1017 | :return: True if field should be included in the result of fields_view_get |
1018 | """ |
1019 | - if node.tag == 'field' and node.get('name') in Model._all_columns: |
1020 | - column = Model._all_columns[node.get('name')].column |
1021 | - if column.groups and not self.user_has_groups( |
1022 | - cr, user, groups=column.groups, context=context): |
1023 | + if node.tag == 'field' and node.get('name') in Model._fields: |
1024 | + field = Model._fields[node.get('name')] |
1025 | + if field.groups and not self.user_has_groups( |
1026 | + cr, user, groups=field.groups, context=context): |
1027 | node.getparent().remove(node) |
1028 | fields.pop(node.get('name'), None) |
1029 | # no point processing view-level ``groups`` anymore, return |
1030 | @@ -565,15 +565,8 @@ |
1031 | fields = xfields |
1032 | if node.get('name'): |
1033 | attrs = {} |
1034 | - try: |
1035 | - if node.get('name') in Model._columns: |
1036 | - column = Model._columns[node.get('name')] |
1037 | - else: |
1038 | - column = Model._inherit_fields[node.get('name')][2] |
1039 | - except Exception: |
1040 | - column = False |
1041 | - |
1042 | - if column: |
1043 | + field = Model._fields.get(node.get('name')) |
1044 | + if field: |
1045 | children = False |
1046 | views = {} |
1047 | for f in node: |
1048 | @@ -581,7 +574,7 @@ |
1049 | node.remove(f) |
1050 | ctx = context.copy() |
1051 | ctx['base_model_name'] = model |
1052 | - xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx) |
1053 | + xarch, xfields = self.postprocess_and_fields(cr, user, field.comodel_name, f, view_id, ctx) |
1054 | views[str(f.tag)] = { |
1055 | 'arch': xarch, |
1056 | 'fields': xfields |
1057 | @@ -649,6 +642,36 @@ |
1058 | orm.transfer_modifiers_to_node(modifiers, node) |
1059 | return fields |
1060 | |
1061 | + def add_on_change(self, cr, user, model_name, arch): |
1062 | + """ Add attribute on_change="1" on fields that are dependencies of |
1063 | + computed fields on the same view. |
1064 | + """ |
1065 | + # map each field object to its corresponding nodes in arch |
1066 | + field_nodes = collections.defaultdict(list) |
1067 | + |
1068 | + def collect(node, model): |
1069 | + if node.tag == 'field': |
1070 | + field = model._fields.get(node.get('name')) |
1071 | + if field: |
1072 | + field_nodes[field].append(node) |
1073 | + if field.relational: |
1074 | + model = self.pool.get(field.comodel_name) |
1075 | + for child in node: |
1076 | + collect(child, model) |
1077 | + |
1078 | + collect(arch, self.pool[model_name]) |
1079 | + |
1080 | + for field, nodes in field_nodes.iteritems(): |
1081 | + # if field should trigger an onchange, add on_change="1" on the |
1082 | + # nodes referring to field |
1083 | + model = self.pool[field.model_name] |
1084 | + if model._has_onchange(field, field_nodes): |
1085 | + for node in nodes: |
1086 | + if not node.get('on_change'): |
1087 | + node.set('on_change', '1') |
1088 | + |
1089 | + return arch |
1090 | + |
1091 | def _disable_workflow_buttons(self, cr, user, model, node): |
1092 | """ Set the buttons in node to readonly if the user can't activate them. """ |
1093 | if model is None or user == 1: |
1094 | @@ -687,7 +710,7 @@ |
1095 | """ |
1096 | fields = {} |
1097 | Model = self.pool.get(model) |
1098 | - if not Model: |
1099 | + if Model is None: |
1100 | self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context) |
1101 | |
1102 | if node.tag == 'diagram': |
1103 | @@ -703,6 +726,7 @@ |
1104 | else: |
1105 | fields = Model.fields_get(cr, user, None, context) |
1106 | |
1107 | + node = self.add_on_change(cr, user, model, node) |
1108 | fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context) |
1109 | node = self._disable_workflow_buttons(cr, user, model, node) |
1110 | if node.tag in ('kanban', 'tree', 'form', 'gantt'): |
1111 | @@ -868,6 +892,7 @@ |
1112 | xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0] |
1113 | return '%s.%s' % (xmlid['module'], xmlid['name']) |
1114 | |
1115 | + @api.cr_uid_ids_context |
1116 | def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None): |
1117 | if isinstance(id_or_xml_id, list): |
1118 | id_or_xml_id = id_or_xml_id[0] |
1119 | |
1120 | === modified file 'openerp/addons/base/ir/ir_values.py' |
1121 | --- openerp/addons/base/ir/ir_values.py 2014-02-13 11:09:37 +0000 |
1122 | +++ openerp/addons/base/ir/ir_values.py 2014-05-19 13:53:27 +0000 |
1123 | @@ -20,6 +20,7 @@ |
1124 | ############################################################################## |
1125 | import pickle |
1126 | |
1127 | +from openerp import tools |
1128 | from openerp.osv import osv, fields |
1129 | from openerp.osv.orm import except_orm |
1130 | |
1131 | @@ -188,6 +189,21 @@ |
1132 | if not cr.fetchone(): |
1133 | cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)') |
1134 | |
1135 | + def create(self, cr, uid, vals, context=None): |
1136 | + res = super(ir_values, self).create(cr, uid, vals, context=context) |
1137 | + self.get_defaults_dict.clear_cache(self) |
1138 | + return res |
1139 | + |
1140 | + def write(self, cr, uid, ids, vals, context=None): |
1141 | + res = super(ir_values, self).write(cr, uid, ids, vals, context=context) |
1142 | + self.get_defaults_dict.clear_cache(self) |
1143 | + return res |
1144 | + |
1145 | + def unlink(self, cr, uid, ids, context=None): |
1146 | + res = super(ir_values, self).unlink(cr, uid, ids, context=context) |
1147 | + self.get_defaults_dict.clear_cache(self) |
1148 | + return res |
1149 | + |
1150 | def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False): |
1151 | """Defines a default value for the given model and field_name. Any previous |
1152 | default for the same scope (model, field_name, value, for_all_users, company_id, condition) |
1153 | @@ -319,6 +335,15 @@ |
1154 | (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8')))) |
1155 | return defaults.values() |
1156 | |
1157 | + # use ormcache: this is called a lot by BaseModel.add_default_value()! |
1158 | + @tools.ormcache(skiparg=2) |
1159 | + def get_defaults_dict(self, cr, uid, model, condition=False): |
1160 | + """ Returns a dictionary mapping field names with their corresponding |
1161 | + default value. This method simply improves the returned value of |
1162 | + :meth:`~.get_defaults`. |
1163 | + """ |
1164 | + return dict((f, v) for i, f, v in self.get_defaults(cr, uid, model, condition)) |
1165 | + |
1166 | def set_action(self, cr, uid, name, action_slot, model, action, res_id=False): |
1167 | """Binds an the given action to the given model's action slot - for later |
1168 | retrieval via :meth:`~.get_actions`. Any existing binding of the same action |
1169 | @@ -395,9 +420,9 @@ |
1170 | if not action['value']: |
1171 | continue # skip if undefined |
1172 | action_model_name, action_id = action['value'].split(',') |
1173 | - action_model = self.pool.get(action_model_name) |
1174 | - if not action_model: |
1175 | + if action_model_name not in self.pool: |
1176 | continue # unknow model? skip it |
1177 | + action_model = self.pool[action_model_name] |
1178 | fields = [field for field in action_model._all_columns if field not in EXCLUDED_FIELDS] |
1179 | # FIXME: needs cleanup |
1180 | try: |
1181 | |
1182 | === modified file 'openerp/addons/base/module/module.py' |
1183 | --- openerp/addons/base/module/module.py 2014-04-15 05:32:50 +0000 |
1184 | +++ openerp/addons/base/module/module.py 2014-05-19 13:53:27 +0000 |
1185 | @@ -48,7 +48,7 @@ |
1186 | from openerp.modules import get_module_resource |
1187 | from openerp.tools.parse_version import parse_version |
1188 | from openerp.tools.translate import _ |
1189 | -from openerp.osv import fields, osv, orm |
1190 | +from openerp.osv import osv, orm, fields, fields2, api |
1191 | |
1192 | _logger = logging.getLogger(__name__) |
1193 | |
1194 | @@ -374,34 +374,41 @@ |
1195 | msg = _('Unable to process module "%s" because an external dependency is not met: %s') |
1196 | raise orm.except_orm(_('Error'), msg % (module_name, e.args[0])) |
1197 | |
1198 | - def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100): |
1199 | + @api.multi |
1200 | + def state_update(self, newstate, states_to_update, level=100): |
1201 | if level < 1: |
1202 | raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !')) |
1203 | + |
1204 | + # whether some modules are installed with demo data |
1205 | demo = False |
1206 | - for module in self.browse(cr, uid, ids, context=context): |
1207 | - mdemo = False |
1208 | + |
1209 | + for module in self: |
1210 | + # determine dependency modules to update/others |
1211 | + update_mods, ready_mods = self.browse(), self.browse() |
1212 | for dep in module.dependencies_id: |
1213 | if dep.state == 'unknown': |
1214 | 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,)) |
1215 | - ids2 = self.search(cr, uid, [('name', '=', dep.name)]) |
1216 | - if dep.state != newstate: |
1217 | - mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level - 1) or mdemo |
1218 | + if dep.depend_id.state == newstate: |
1219 | + ready_mods += dep.depend_id |
1220 | else: |
1221 | - od = self.browse(cr, uid, ids2)[0] |
1222 | - mdemo = od.demo or mdemo |
1223 | - |
1224 | + update_mods += dep.depend_id |
1225 | + |
1226 | + # update dependency modules that require it, and determine demo for module |
1227 | + update_demo = update_mods.state_update(newstate, states_to_update, level=level-1) |
1228 | + module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods) |
1229 | + demo = demo or module_demo |
1230 | + |
1231 | + # check dependencies and update module itself |
1232 | self.check_external_dependencies(module.name, newstate) |
1233 | - if not module.dependencies_id: |
1234 | - mdemo = module.demo |
1235 | if module.state in states_to_update: |
1236 | - self.write(cr, uid, [module.id], {'state': newstate, 'demo': mdemo}) |
1237 | - demo = demo or mdemo |
1238 | + module.write({'state': newstate, 'demo': module_demo}) |
1239 | + |
1240 | return demo |
1241 | |
1242 | def button_install(self, cr, uid, ids, context=None): |
1243 | |
1244 | # Mark the given modules to be installed. |
1245 | - self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context) |
1246 | + self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context=context) |
1247 | |
1248 | # Mark (recursively) the newly satisfied modules to also be installed |
1249 | |
1250 | @@ -527,7 +534,7 @@ |
1251 | |
1252 | def button_upgrade(self, cr, uid, ids, context=None): |
1253 | depobj = self.pool.get('ir.module.module.dependency') |
1254 | - todo = self.browse(cr, uid, ids, context=context) |
1255 | + todo = list(self.browse(cr, uid, ids, context=context)) |
1256 | self.update_list(cr, uid) |
1257 | |
1258 | i = 0 |
1259 | @@ -729,6 +736,7 @@ |
1260 | cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (mod_browse.id, dep)) |
1261 | for dep in (existing - needed): |
1262 | cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (mod_browse.id, dep)) |
1263 | + self.invalidate_cache(cr, uid, ['dependencies_id'], [mod_browse.id]) |
1264 | |
1265 | def _update_category(self, cr, uid, mod_browse, category='Uncategorized'): |
1266 | current_category = mod_browse.category_id |
1267 | @@ -757,37 +765,49 @@ |
1268 | if not mod.description: |
1269 | _logger.warning('module %s: description is empty !', mod.name) |
1270 | |
1271 | -class module_dependency(osv.osv): |
1272 | + |
1273 | +DEP_STATES = [ |
1274 | + ('uninstallable', 'Uninstallable'), |
1275 | + ('uninstalled', 'Not Installed'), |
1276 | + ('installed', 'Installed'), |
1277 | + ('to upgrade', 'To be upgraded'), |
1278 | + ('to remove', 'To be removed'), |
1279 | + ('to install', 'To be installed'), |
1280 | + ('unknown', 'Unknown'), |
1281 | +] |
1282 | + |
1283 | +class module_dependency(osv.Model): |
1284 | _name = "ir.module.module.dependency" |
1285 | _description = "Module dependency" |
1286 | |
1287 | - def _state(self, cr, uid, ids, name, args, context=None): |
1288 | - result = {} |
1289 | - mod_obj = self.pool.get('ir.module.module') |
1290 | - for md in self.browse(cr, uid, ids): |
1291 | - ids = mod_obj.search(cr, uid, [('name', '=', md.name)]) |
1292 | - if ids: |
1293 | - result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state'] |
1294 | - else: |
1295 | - result[md.id] = 'unknown' |
1296 | - return result |
1297 | - |
1298 | - _columns = { |
1299 | - # The dependency name |
1300 | - 'name': fields.char('Name', size=128, select=True), |
1301 | - |
1302 | - # The module that depends on it |
1303 | - 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'), |
1304 | - |
1305 | - 'state': fields.function(_state, type='selection', selection=[ |
1306 | - ('uninstallable', 'Uninstallable'), |
1307 | - ('uninstalled', 'Not Installed'), |
1308 | - ('installed', 'Installed'), |
1309 | - ('to upgrade', 'To be upgraded'), |
1310 | - ('to remove', 'To be removed'), |
1311 | - ('to install', 'To be installed'), |
1312 | - ('unknown', 'Unknown'), |
1313 | - ], string='Status', readonly=True, select=True), |
1314 | - } |
1315 | + # the dependency name |
1316 | + name = fields2.Char(size=128) |
1317 | + |
1318 | + # the module that depends on it |
1319 | + module_id = fields2.Many2one('ir.module.module', 'Module', ondelete='cascade') |
1320 | + |
1321 | + # the module corresponding to the dependency, and its status |
1322 | + depend_id = fields2.Many2one('ir.module.module', 'Dependency', |
1323 | + compute='_compute_depend', readonly=True, store=False) |
1324 | + state = fields2.Selection(DEP_STATES, string='Status', |
1325 | + compute='_compute_state', readonly=True, store=False) |
1326 | + |
1327 | + @api.multi |
1328 | + @api.depends('name') |
1329 | + def _compute_depend(self): |
1330 | + # retrieve all modules corresponding to the dependency names |
1331 | + names = list(set(dep.name for dep in self)) |
1332 | + mods = self.env['ir.module.module'].search([('name', 'in', names)]) |
1333 | + |
1334 | + # index modules by name, and assign dependencies |
1335 | + name_mod = dict((mod.name, mod) for mod in mods) |
1336 | + for dep in self: |
1337 | + dep.depend_id = name_mod.get(dep.name) |
1338 | + |
1339 | + @api.one |
1340 | + @api.depends('depend_id.state') |
1341 | + def _compute_state(self): |
1342 | + self.state = self.depend_id.state or 'unknown' |
1343 | + |
1344 | |
1345 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
1346 | |
1347 | === modified file 'openerp/addons/base/res/ir_property.py' |
1348 | --- openerp/addons/base/res/ir_property.py 2014-05-06 08:41:38 +0000 |
1349 | +++ openerp/addons/base/res/ir_property.py 2014-05-19 13:53:27 +0000 |
1350 | @@ -21,8 +21,7 @@ |
1351 | |
1352 | import time |
1353 | |
1354 | -from openerp.osv import osv, fields |
1355 | -from openerp.osv.orm import browse_record, browse_null |
1356 | +from openerp.osv import osv, orm, fields |
1357 | from openerp.tools.misc import attrgetter |
1358 | |
1359 | # ------------------------------------------------------------------------- |
1360 | @@ -97,7 +96,7 @@ |
1361 | raise osv.except_osv('Error', 'Invalid type') |
1362 | |
1363 | if field == 'value_reference': |
1364 | - if isinstance(value, browse_record): |
1365 | + if isinstance(value, orm.BaseModel): |
1366 | value = '%s,%d' % (value._name, value.id) |
1367 | elif isinstance(value, (int, long)): |
1368 | field_id = values.get('fields_id') |
1369 | @@ -132,7 +131,7 @@ |
1370 | return record.value_binary |
1371 | elif record.type == 'many2one': |
1372 | if not record.value_reference: |
1373 | - return browse_null() |
1374 | + return False |
1375 | model, resource_id = record.value_reference.split(',') |
1376 | return self.pool.get(model).browse(cr, uid, int(resource_id), context=context) |
1377 | elif record.type == 'datetime': |
1378 | |
1379 | === modified file 'openerp/addons/base/res/res_company.py' |
1380 | --- openerp/addons/base/res/res_company.py 2014-03-11 13:38:50 +0000 |
1381 | +++ openerp/addons/base/res/res_company.py 2014-05-19 13:53:27 +0000 |
1382 | @@ -84,7 +84,7 @@ |
1383 | if company.partner_id: |
1384 | address_data = part_obj.address_get(cr, openerp.SUPERUSER_ID, [company.partner_id.id], adr_pref=['default']) |
1385 | if address_data['default']: |
1386 | - address = part_obj.read(cr, openerp.SUPERUSER_ID, address_data['default'], field_names, context=context) |
1387 | + address = part_obj.read(cr, openerp.SUPERUSER_ID, [address_data['default']], field_names, context=context)[0] |
1388 | for field in field_names: |
1389 | result[company.id][field] = address[field] or False |
1390 | return result |
1391 | @@ -176,6 +176,7 @@ |
1392 | res += '\n%s: %s' % (title, ', '.join(name for id, name in account_names)) |
1393 | |
1394 | return {'value': {'rml_footer': res, 'rml_footer_readonly': res}} |
1395 | + |
1396 | def onchange_state(self, cr, uid, ids, state_id, context=None): |
1397 | if state_id: |
1398 | return {'value':{'country_id': self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id }} |
1399 | @@ -209,8 +210,7 @@ |
1400 | return res |
1401 | |
1402 | def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100): |
1403 | - if context is None: |
1404 | - context = {} |
1405 | + context = dict(context or {}) |
1406 | if context.pop('user_preference', None): |
1407 | # We browse as superuser. Otherwise, the user would be able to |
1408 | # select only the currently visible companies (according to rules, |
1409 | |
1410 | === modified file 'openerp/addons/base/res/res_config.py' |
1411 | --- openerp/addons/base/res/res_config.py 2014-03-26 14:05:09 +0000 |
1412 | +++ openerp/addons/base/res/res_config.py 2014-05-19 13:53:27 +0000 |
1413 | @@ -293,10 +293,10 @@ |
1414 | def _already_installed(self, cr, uid, context=None): |
1415 | """ For each module (boolean fields in a res.config.installer), |
1416 | check if it's already installed (either 'to install', 'to upgrade' |
1417 | - or 'installed') and if it is return the module's browse_record |
1418 | + or 'installed') and if it is return the module's record |
1419 | |
1420 | :returns: a list of all installed modules in this installer |
1421 | - :rtype: [browse_record] |
1422 | + :rtype: recordset (collection of Record) |
1423 | """ |
1424 | modules = self.pool['ir.module.module'] |
1425 | |
1426 | @@ -332,7 +332,7 @@ |
1427 | for installer in self.read(cr, uid, ids, context=context) |
1428 | for module_name, to_install in installer.iteritems() |
1429 | if module_name != 'id' |
1430 | - if type(self._columns[module_name]) is fields.boolean |
1431 | + if type(self._columns.get(module_name)) is fields.boolean |
1432 | if to_install) |
1433 | |
1434 | hooks_results = set() |
1435 | |
1436 | === modified file 'openerp/addons/base/res/res_currency.py' |
1437 | --- openerp/addons/base/res/res_currency.py 2014-05-01 18:42:17 +0000 |
1438 | +++ openerp/addons/base/res/res_currency.py 2014-05-19 13:53:27 +0000 |
1439 | @@ -22,6 +22,7 @@ |
1440 | import re |
1441 | import time |
1442 | |
1443 | +from openerp import api, fields as fields2 |
1444 | from openerp import tools |
1445 | from openerp.osv import fields, osv |
1446 | from openerp.tools import float_round, float_is_zero, float_compare |
1447 | @@ -79,7 +80,6 @@ |
1448 | 'rounding': fields.float('Rounding Factor', digits=(12,6)), |
1449 | 'active': fields.boolean('Active'), |
1450 | 'company_id':fields.many2one('res.company', 'Company'), |
1451 | - 'date': fields.date('Date'), |
1452 | 'base': fields.boolean('Base'), |
1453 | '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.") |
1454 | } |
1455 | @@ -111,19 +111,12 @@ |
1456 | ON res_currency |
1457 | (name, (COALESCE(company_id,-1)))""") |
1458 | |
1459 | - def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'): |
1460 | - res = super(res_currency, self).read(cr, user, ids, fields, context, load) |
1461 | - currency_rate_obj = self.pool.get('res.currency.rate') |
1462 | - values = res |
1463 | - if not isinstance(values, list): |
1464 | - values = [values] |
1465 | - for r in values: |
1466 | - if r.__contains__('rate_ids'): |
1467 | - rates=r['rate_ids'] |
1468 | - if rates: |
1469 | - currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name'] |
1470 | - r['date'] = currency_date |
1471 | - return res |
1472 | + date = fields2.Date(compute='compute_date', store=True) |
1473 | + |
1474 | + @api.one |
1475 | + @api.depends('rate_ids.name') |
1476 | + def compute_date(self): |
1477 | + self.date = self.rate_ids.name |
1478 | |
1479 | def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): |
1480 | if not args: |
1481 | @@ -145,16 +138,38 @@ |
1482 | reads = self.read(cr, uid, ids, ['name','symbol'], context=context, load='_classic_write') |
1483 | return [(x['id'], tools.ustr(x['name'])) for x in reads] |
1484 | |
1485 | + @api.new |
1486 | + def round(self, amount): |
1487 | + """ Return `amount` rounded according to currency `self`. """ |
1488 | + return float_round(amount, precision_rounding=self.rounding) |
1489 | + |
1490 | + @round.old |
1491 | def round(self, cr, uid, currency, amount): |
1492 | """Return ``amount`` rounded according to ``currency``'s |
1493 | rounding rules. |
1494 | |
1495 | - :param browse_record currency: currency for which we are rounding |
1496 | + :param Record currency: currency for which we are rounding |
1497 | :param float amount: the amount to round |
1498 | :return: rounded float |
1499 | """ |
1500 | return float_round(amount, precision_rounding=currency.rounding) |
1501 | |
1502 | + @api.new |
1503 | + def compare_amounts(self, amount1, amount2): |
1504 | + """ Compare `amount1` and `amount2` after rounding them according to |
1505 | + `self`'s precision. An amount is considered lower/greater than |
1506 | + another amount if their rounded value is different. This is not the |
1507 | + same as having a non-zero difference! |
1508 | + |
1509 | + For example 1.432 and 1.431 are equal at 2 digits precision, so this |
1510 | + method would return 0. However 0.006 and 0.002 are considered |
1511 | + different (returns 1) because they respectively round to 0.01 and |
1512 | + 0.0, even though 0.006-0.002 = 0.004 which would be considered zero |
1513 | + at 2 digits precision. |
1514 | + """ |
1515 | + return float_compare(amount1, amount2, precision_rounding=self.rounding) |
1516 | + |
1517 | + @compare_amounts.old |
1518 | def compare_amounts(self, cr, uid, currency, amount1, amount2): |
1519 | """Compare ``amount1`` and ``amount2`` after rounding them according to the |
1520 | given currency's precision.. |
1521 | @@ -167,7 +182,7 @@ |
1522 | they respectively round to 0.01 and 0.0, even though |
1523 | 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. |
1524 | |
1525 | - :param browse_record currency: currency for which we are rounding |
1526 | + :param Record currency: currency for which we are rounding |
1527 | :param float amount1: first amount to compare |
1528 | :param float amount2: second amount to compare |
1529 | :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, |
1530 | @@ -176,6 +191,19 @@ |
1531 | """ |
1532 | return float_compare(amount1, amount2, precision_rounding=currency.rounding) |
1533 | |
1534 | + @api.new |
1535 | + def is_zero(self, amount): |
1536 | + """ Return true if `amount` is small enough to be treated as zero |
1537 | + according to currency `self`'s rounding rules. |
1538 | + |
1539 | + Warning: ``is_zero(amount1-amount2)`` is not always equivalent to |
1540 | + ``compare_amounts(amount1,amount2) == 0``, as the former will round |
1541 | + after computing the difference, while the latter will round before, |
1542 | + giving different results, e.g., 0.006 and 0.002 at 2 digits precision. |
1543 | + """ |
1544 | + return float_is_zero(amount, precision_rounding=self.rounding) |
1545 | + |
1546 | + @is_zero.old |
1547 | def is_zero(self, cr, uid, currency, amount): |
1548 | """Returns true if ``amount`` is small enough to be treated as |
1549 | zero according to ``currency``'s rounding rules. |
1550 | @@ -185,7 +213,7 @@ |
1551 | computing the difference, while the latter will round before, giving |
1552 | different results for e.g. 0.006 and 0.002 at 2 digits precision. |
1553 | |
1554 | - :param browse_record currency: currency for which we are rounding |
1555 | + :param Record currency: currency for which we are rounding |
1556 | :param float amount: amount to compare with currency's zero |
1557 | """ |
1558 | return float_is_zero(amount, precision_rounding=currency.rounding) |
1559 | @@ -211,10 +239,23 @@ |
1560 | 'at the date: %s') % (currency_symbol, date)) |
1561 | return to_currency.rate/from_currency.rate |
1562 | |
1563 | + @api.new |
1564 | + def compute(self, from_amount, to_currency, round=True): |
1565 | + """ Convert `from_amount` from currency `self` to `to_currency`. """ |
1566 | + assert self, "compute from unknown currency" |
1567 | + assert to_currency, "compute to unknown currency" |
1568 | + # apply conversion rate |
1569 | + if self == to_currency: |
1570 | + to_amount = from_amount |
1571 | + else: |
1572 | + to_amount = from_amount * self._get_conversion_rate(self, to_currency) |
1573 | + # apply rounding |
1574 | + return to_currency.round(to_amount) if round else to_amount |
1575 | + |
1576 | + @compute.old |
1577 | def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount, |
1578 | round=True, currency_rate_type_from=False, currency_rate_type_to=False, context=None): |
1579 | - if not context: |
1580 | - context = {} |
1581 | + context = dict(context or {}) |
1582 | if not from_currency_id: |
1583 | from_currency_id = to_currency_id |
1584 | if not to_currency_id: |
1585 | @@ -253,7 +294,7 @@ |
1586 | '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"), |
1587 | } |
1588 | _defaults = { |
1589 | - 'name': lambda *a: time.strftime('%Y-%m-%d'), |
1590 | + 'name': lambda *a: time.strftime('%Y-%m-%d 00:00:00'), |
1591 | } |
1592 | _order = "name desc" |
1593 | |
1594 | |
1595 | === modified file 'openerp/addons/base/res/res_partner.py' |
1596 | --- openerp/addons/base/res/res_partner.py 2014-04-17 14:55:22 +0000 |
1597 | +++ openerp/addons/base/res/res_partner.py 2014-05-19 13:53:27 +0000 |
1598 | @@ -26,43 +26,44 @@ |
1599 | |
1600 | import openerp |
1601 | from openerp import SUPERUSER_ID |
1602 | -from openerp import tools |
1603 | +from openerp import tools, model, multi, one, returns |
1604 | from openerp.osv import osv, fields |
1605 | from openerp.osv.expression import get_unaccent_wrapper |
1606 | from openerp.tools.translate import _ |
1607 | |
1608 | +ADDRESS_FORMAT_LAYOUTS = { |
1609 | + '%(city)s %(state_code)s\n%(zip)s': """ |
1610 | + <div class="address_format"> |
1611 | + <field name="city" placeholder="City" style="width: 50%%"/> |
1612 | + <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/> |
1613 | + <br/> |
1614 | + <field name="zip" placeholder="ZIP"/> |
1615 | + </div> |
1616 | + """, |
1617 | + '%(zip)s %(city)s': """ |
1618 | + <div class="address_format"> |
1619 | + <field name="zip" placeholder="ZIP" style="width: 40%%"/> |
1620 | + <field name="city" placeholder="City" style="width: 57%%"/> |
1621 | + <br/> |
1622 | + <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/> |
1623 | + </div> |
1624 | + """, |
1625 | + '%(city)s\n%(state_name)s\n%(zip)s': """ |
1626 | + <div class="address_format"> |
1627 | + <field name="city" placeholder="City"/> |
1628 | + <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/> |
1629 | + <field name="zip" placeholder="ZIP"/> |
1630 | + </div> |
1631 | + """ |
1632 | +} |
1633 | + |
1634 | + |
1635 | class format_address(object): |
1636 | - def fields_view_get_address(self, cr, uid, arch, context={}): |
1637 | - user_obj = self.pool['res.users'] |
1638 | - fmt = user_obj.browse(cr, SUPERUSER_ID, uid, context).company_id.country_id |
1639 | - fmt = fmt and fmt.address_format |
1640 | - layouts = { |
1641 | - '%(city)s %(state_code)s\n%(zip)s': """ |
1642 | - <div class="address_format"> |
1643 | - <field name="city" placeholder="City" style="width: 50%%"/> |
1644 | - <field name="state_id" class="oe_no_button" placeholder="State" style="width: 47%%" options='{"no_open": true}'/> |
1645 | - <br/> |
1646 | - <field name="zip" placeholder="ZIP"/> |
1647 | - </div> |
1648 | - """, |
1649 | - '%(zip)s %(city)s': """ |
1650 | - <div class="address_format"> |
1651 | - <field name="zip" placeholder="ZIP" style="width: 40%%"/> |
1652 | - <field name="city" placeholder="City" style="width: 57%%"/> |
1653 | - <br/> |
1654 | - <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/> |
1655 | - </div> |
1656 | - """, |
1657 | - '%(city)s\n%(state_name)s\n%(zip)s': """ |
1658 | - <div class="address_format"> |
1659 | - <field name="city" placeholder="City"/> |
1660 | - <field name="state_id" class="oe_no_button" placeholder="State" options='{"no_open": true}'/> |
1661 | - <field name="zip" placeholder="ZIP"/> |
1662 | - </div> |
1663 | - """ |
1664 | - } |
1665 | - for k,v in layouts.items(): |
1666 | - if fmt and (k in fmt): |
1667 | + @model |
1668 | + def fields_view_get_address(self, arch): |
1669 | + fmt = self.env.user.company_id.country_id.address_format or '' |
1670 | + for k, v in ADDRESS_FORMAT_LAYOUTS.items(): |
1671 | + if k in fmt: |
1672 | doc = etree.fromstring(arch) |
1673 | for node in doc.xpath("//div[@class='address_format']"): |
1674 | tree = etree.fromstring(v) |
1675 | @@ -72,53 +73,53 @@ |
1676 | return arch |
1677 | |
1678 | |
1679 | -def _tz_get(self,cr,uid, context=None): |
1680 | +@model |
1681 | +def _tz_get(self): |
1682 | # put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728 |
1683 | return [(tz,tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')] |
1684 | |
1685 | -class res_partner_category(osv.osv): |
1686 | + |
1687 | +class res_partner_category(osv.Model): |
1688 | |
1689 | def name_get(self, cr, uid, ids, context=None): |
1690 | - """Return the categories' display name, including their direct |
1691 | - parent by default. |
1692 | + """ Return the categories' display name, including their direct |
1693 | + parent by default. |
1694 | |
1695 | - :param dict context: the ``partner_category_display`` key can be |
1696 | - used to select the short version of the |
1697 | - category name (without the direct parent), |
1698 | - when set to ``'short'``. The default is |
1699 | - the long version.""" |
1700 | + If ``context['partner_category_display']`` is ``'short'``, the short |
1701 | + version of the category name (without the direct parent) is used. |
1702 | + The default is the long version. |
1703 | + """ |
1704 | + if not isinstance(ids, list): |
1705 | + ids = [ids] |
1706 | if context is None: |
1707 | context = {} |
1708 | + |
1709 | if context.get('partner_category_display') == 'short': |
1710 | return super(res_partner_category, self).name_get(cr, uid, ids, context=context) |
1711 | - if isinstance(ids, (int, long)): |
1712 | - ids = [ids] |
1713 | - reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context) |
1714 | + |
1715 | res = [] |
1716 | - for record in reads: |
1717 | - name = record['name'] |
1718 | - if record['parent_id']: |
1719 | - name = record['parent_id'][1] + ' / ' + name |
1720 | - res.append((record['id'], name)) |
1721 | + for category in self.browse(cr, uid, ids, context=context): |
1722 | + names = [] |
1723 | + current = category |
1724 | + while current: |
1725 | + names.append(current.name) |
1726 | + current = current.parent_id |
1727 | + res.append((category.id, ' / '.join(reversed(names)))) |
1728 | return res |
1729 | |
1730 | - def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): |
1731 | - if not args: |
1732 | - args = [] |
1733 | - if not context: |
1734 | - context = {} |
1735 | + @model |
1736 | + def name_search(self, name, args=None, operator='ilike', limit=100): |
1737 | + args = args or [] |
1738 | if name: |
1739 | # Be sure name_search is symetric to name_get |
1740 | name = name.split(' / ')[-1] |
1741 | - ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context) |
1742 | - else: |
1743 | - ids = self.search(cr, uid, args, limit=limit, context=context) |
1744 | - return self.name_get(cr, uid, ids, context) |
1745 | - |
1746 | - |
1747 | - def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): |
1748 | - res = self.name_get(cr, uid, ids, context=context) |
1749 | - return dict(res) |
1750 | + args = [('name', operator, name)] + args |
1751 | + categories = self.search(args, limit=limit) |
1752 | + return categories.name_get() |
1753 | + |
1754 | + @multi |
1755 | + def _name_get_fnc(self, field_name, arg): |
1756 | + return dict(self.name_get()) |
1757 | |
1758 | _description = 'Partner Tags' |
1759 | _name = 'res.partner.category' |
1760 | @@ -142,6 +143,7 @@ |
1761 | _parent_order = 'name' |
1762 | _order = 'parent_left' |
1763 | |
1764 | + |
1765 | class res_partner_title(osv.osv): |
1766 | _name = 'res.partner.title' |
1767 | _order = 'name' |
1768 | @@ -154,16 +156,17 @@ |
1769 | 'domain': 'contact', |
1770 | } |
1771 | |
1772 | -def _lang_get(self, cr, uid, context=None): |
1773 | - lang_pool = self.pool['res.lang'] |
1774 | - ids = lang_pool.search(cr, uid, [], context=context) |
1775 | - res = lang_pool.read(cr, uid, ids, ['code', 'name'], context) |
1776 | - return [(r['code'], r['name']) for r in res] |
1777 | + |
1778 | +@model |
1779 | +def _lang_get(self): |
1780 | + languages = self.env['res.lang'].search([]) |
1781 | + return [(language.code, language.name) for language in languages] |
1782 | |
1783 | # fields copy if 'use_parent_address' is checked |
1784 | ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id') |
1785 | |
1786 | -class res_partner(osv.osv, format_address): |
1787 | + |
1788 | +class res_partner(osv.Model, format_address): |
1789 | _description = 'Partner' |
1790 | _name = "res.partner" |
1791 | |
1792 | @@ -173,26 +176,23 @@ |
1793 | res[partner.id] = self._display_address(cr, uid, partner, context=context) |
1794 | return res |
1795 | |
1796 | - def _get_image(self, cr, uid, ids, name, args, context=None): |
1797 | - result = dict.fromkeys(ids, False) |
1798 | - for obj in self.browse(cr, uid, ids, context=context): |
1799 | - result[obj.id] = tools.image_get_resized_images(obj.image) |
1800 | - return result |
1801 | - |
1802 | - def _get_tz_offset(self, cr, uid, ids, name, args, context=None): |
1803 | - result = dict.fromkeys(ids, False) |
1804 | - for obj in self.browse(cr, uid, ids, context=context): |
1805 | - result[obj.id] = datetime.datetime.now(pytz.timezone(obj.tz or 'GMT')).strftime('%z') |
1806 | - return result |
1807 | - |
1808 | - def _set_image(self, cr, uid, id, name, value, args, context=None): |
1809 | - return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) |
1810 | - |
1811 | - def _has_image(self, cr, uid, ids, name, args, context=None): |
1812 | - result = {} |
1813 | - for obj in self.browse(cr, uid, ids, context=context): |
1814 | - result[obj.id] = obj.image != False |
1815 | - return result |
1816 | + @multi |
1817 | + def _get_tz_offset(self, name, args): |
1818 | + return dict( |
1819 | + (p.id, datetime.datetime.now(pytz.timezone(p.tz or 'GMT')).strftime('%z')) |
1820 | + for p in self) |
1821 | + |
1822 | + @multi |
1823 | + def _get_image(self, name, args): |
1824 | + return dict((p.id, tools.image_get_resized_images(p.image)) for p in self) |
1825 | + |
1826 | + @one |
1827 | + def _set_image(self, name, value, args): |
1828 | + return self.write({'image': tools.image_resize_image_big(value)}) |
1829 | + |
1830 | + @multi |
1831 | + def _has_image(self, name, args): |
1832 | + return dict((p.id, bool(p.image)) for p in self) |
1833 | |
1834 | def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None): |
1835 | """ Returns the partner that is considered the commercial |
1836 | @@ -303,16 +303,15 @@ |
1837 | 'commercial_partner_id': fields.function(_commercial_partner_id, type='many2one', relation='res.partner', string='Commercial Entity', store=_commercial_partner_store_triggers) |
1838 | } |
1839 | |
1840 | - def _default_category(self, cr, uid, context=None): |
1841 | - if context is None: |
1842 | - context = {} |
1843 | - if context.get('category_id'): |
1844 | - return [context['category_id']] |
1845 | - return False |
1846 | + @model |
1847 | + def _default_category(self): |
1848 | + category_id = self.env.context.get('category_id', False) |
1849 | + return [category_id] if category_id else False |
1850 | |
1851 | - def _get_default_image(self, cr, uid, is_company, context=None, colorize=False): |
1852 | - img_path = openerp.modules.get_module_resource('base', 'static/src/img', |
1853 | - ('company_image.png' if is_company else 'avatar.png')) |
1854 | + @model |
1855 | + def _get_default_image(self, is_company, colorize=False): |
1856 | + img_path = openerp.modules.get_module_resource( |
1857 | + 'base', 'static/src/img', 'company_image.png' if is_company else 'avatar.png') |
1858 | with open(img_path, 'rb') as f: |
1859 | image = f.read() |
1860 | |
1861 | @@ -330,13 +329,17 @@ |
1862 | res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context) |
1863 | return res |
1864 | |
1865 | + @model |
1866 | + def _default_company(self): |
1867 | + return self.env['res.company']._company_default_get('res.partner') |
1868 | + |
1869 | _defaults = { |
1870 | 'active': True, |
1871 | - 'lang': lambda self, cr, uid, ctx: ctx.get('lang', 'en_US'), |
1872 | - 'tz': lambda self, cr, uid, ctx: ctx.get('tz', False), |
1873 | + 'lang': model(lambda self: self.env.lang), |
1874 | + 'tz': model(lambda self: self.env.context.get('tz', False)), |
1875 | 'customer': True, |
1876 | 'category_id': _default_category, |
1877 | - 'company_id': lambda self, cr, uid, ctx: self.pool['res.company']._company_default_get(cr, uid, 'res.partner', context=ctx), |
1878 | + 'company_id': _default_company, |
1879 | 'color': 0, |
1880 | 'is_company': False, |
1881 | 'type': 'contact', # type 'default' is wildcard and thus inappropriate |
1882 | @@ -348,15 +351,15 @@ |
1883 | (osv.osv._check_recursion, 'You cannot create recursive Partner hierarchies.', ['parent_id']), |
1884 | ] |
1885 | |
1886 | - def copy(self, cr, uid, id, default=None, context=None): |
1887 | - if default is None: |
1888 | - default = {} |
1889 | + @one |
1890 | + def copy(self, default=None): |
1891 | + default = dict(default or {}) |
1892 | default['user_ids'] = False |
1893 | - name = self.read(cr, uid, [id], ['name'], context)[0]['name'] |
1894 | - default.update({'name': _('%s (copy)') % name}) |
1895 | - return super(res_partner, self).copy(cr, uid, id, default, context) |
1896 | + default['name'] = _('%s (copy)') % self.name |
1897 | + return super(res_partner, self).copy(default) |
1898 | |
1899 | - def onchange_type(self, cr, uid, ids, is_company, context=None): |
1900 | + @multi |
1901 | + def onchange_type(self, is_company): |
1902 | value = {} |
1903 | value['title'] = False |
1904 | if is_company: |
1905 | @@ -388,10 +391,11 @@ |
1906 | result['value'] = {'use_parent_address': False} |
1907 | return result |
1908 | |
1909 | - def onchange_state(self, cr, uid, ids, state_id, context=None): |
1910 | + @multi |
1911 | + def onchange_state(self, state_id): |
1912 | if state_id: |
1913 | - country_id = self.pool['res.country.state'].browse(cr, uid, state_id, context).country_id.id |
1914 | - return {'value':{'country_id':country_id}} |
1915 | + state = self.env['res.country.state'].browse(state_id) |
1916 | + return {'value': {'country_id': state.country_id.id}} |
1917 | return {} |
1918 | |
1919 | def _check_ean_key(self, cr, uid, ids, context=None): |
1920 | @@ -509,30 +513,32 @@ |
1921 | if not parent.is_company: |
1922 | parent.write({'is_company': True}) |
1923 | |
1924 | - def write(self, cr, uid, ids, vals, context=None): |
1925 | - if isinstance(ids, (int, long)): |
1926 | - ids = [ids] |
1927 | - #res.partner must only allow to set the company_id of a partner if it |
1928 | - #is the same as the company of all users that inherit from this partner |
1929 | - #(this is to allow the code from res_users to write to the partner!) or |
1930 | - #if setting the company_id to False (this is compatible with any user company) |
1931 | + @multi |
1932 | + def write(self, vals): |
1933 | + # res.partner must only allow to set the company_id of a partner if it |
1934 | + # is the same as the company of all users that inherit from this partner |
1935 | + # (this is to allow the code from res_users to write to the partner!) or |
1936 | + # if setting the company_id to False (this is compatible with any user |
1937 | + # company) |
1938 | if vals.get('company_id'): |
1939 | - for partner in self.browse(cr, uid, ids, context=context): |
1940 | + company = self.env['res.company'].browse(vals['company_id']) |
1941 | + for partner in self: |
1942 | if partner.user_ids: |
1943 | - user_companies = set([user.company_id.id for user in partner.user_ids]) |
1944 | - if len(user_companies) > 1 or vals['company_id'] not in user_companies: |
1945 | + companies = set(user.company_id for user in partner.user_ids) |
1946 | + if len(companies) > 1 or company not in companies: |
1947 | raise osv.except_osv(_("Warning"),_("You can not change the company as the partner/user has multiple user linked with different companies.")) |
1948 | - result = super(res_partner,self).write(cr, uid, ids, vals, context=context) |
1949 | - for partner in self.browse(cr, uid, ids, context=context): |
1950 | - self._fields_sync(cr, uid, partner, vals, context) |
1951 | + |
1952 | + result = super(res_partner, self).write(vals) |
1953 | + for partner in self: |
1954 | + self._fields_sync(partner, vals) |
1955 | return result |
1956 | |
1957 | - def create(self, cr, uid, vals, context=None): |
1958 | - new_id = super(res_partner, self).create(cr, uid, vals, context=context) |
1959 | - partner = self.browse(cr, uid, new_id, context=context) |
1960 | - self._fields_sync(cr, uid, partner, vals, context) |
1961 | - self._handle_first_contact_creation(cr, uid, partner, context) |
1962 | - return new_id |
1963 | + @model |
1964 | + def create(self, vals): |
1965 | + partner = super(res_partner, self).create(vals) |
1966 | + self._fields_sync(partner, vals) |
1967 | + self._handle_first_contact_creation(partner) |
1968 | + return partner |
1969 | |
1970 | def open_commercial_entity(self, cr, uid, ids, context=None): |
1971 | """ Utility method used to add an "Open Company" button in partner views """ |
1972 | @@ -736,14 +742,11 @@ |
1973 | return False |
1974 | return _('Partners: ')+self.pool['res.partner.category'].browse(cr, uid, context['category_id'], context).name |
1975 | |
1976 | - def main_partner(self, cr, uid): |
1977 | - ''' Return the id of the main partner |
1978 | - ''' |
1979 | - model_data = self.pool['ir.model.data'] |
1980 | - return model_data.browse(cr, uid, |
1981 | - model_data.search(cr, uid, [('module','=','base'), |
1982 | - ('name','=','main_partner')])[0], |
1983 | - ).res_id |
1984 | + @model |
1985 | + @returns('self') |
1986 | + def main_partner(self): |
1987 | + ''' Return the main partner ''' |
1988 | + return self.env.ref('base.main_partner') |
1989 | |
1990 | def _display_address(self, cr, uid, address, without_company=False, context=None): |
1991 | |
1992 | @@ -759,14 +762,14 @@ |
1993 | |
1994 | # get the information that will be injected into the display format |
1995 | # get the address format |
1996 | - address_format = address.country_id and address.country_id.address_format or \ |
1997 | + address_format = address.country_id.address_format or \ |
1998 | "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s" |
1999 | args = { |
2000 | - 'state_code': address.state_id and address.state_id.code or '', |
2001 | - 'state_name': address.state_id and address.state_id.name or '', |
2002 | - 'country_code': address.country_id and address.country_id.code or '', |
2003 | - 'country_name': address.country_id and address.country_id.name or '', |
2004 | - 'company_name': address.parent_id and address.parent_id.name or '', |
2005 | + 'state_code': address.state_id.code or '', |
2006 | + 'state_name': address.state_id.name or '', |
2007 | + 'country_code': address.country_id.code or '', |
2008 | + 'country_name': address.country_id.name or '', |
2009 | + 'company_name': address.parent_id.name or '', |
2010 | } |
2011 | for field in self._address_fields(cr, uid, context=context): |
2012 | args[field] = getattr(address, field) or '' |
2013 | |
2014 | === modified file 'openerp/addons/base/res/res_users.py' |
2015 | --- openerp/addons/base/res/res_users.py 2014-05-12 08:05:23 +0000 |
2016 | +++ openerp/addons/base/res/res_users.py 2014-05-19 13:53:27 +0000 |
2017 | @@ -25,11 +25,10 @@ |
2018 | from lxml.builder import E |
2019 | |
2020 | import openerp |
2021 | -from openerp import SUPERUSER_ID |
2022 | +from openerp import SUPERUSER_ID, BaseModel |
2023 | from openerp import tools |
2024 | import openerp.exceptions |
2025 | -from openerp.osv import fields,osv, expression |
2026 | -from openerp.osv.orm import browse_record |
2027 | +from openerp.osv import fields, osv, expression |
2028 | from openerp.tools.translate import _ |
2029 | |
2030 | _logger = logging.getLogger(__name__) |
2031 | @@ -226,9 +225,8 @@ |
2032 | def _get_company(self,cr, uid, context=None, uid2=False): |
2033 | if not uid2: |
2034 | uid2 = uid |
2035 | - user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context) |
2036 | - company_id = user.get('company_id', False) |
2037 | - return company_id and company_id[0] or False |
2038 | + user = self.pool['res.users'].browse(cr, uid, uid2, context) |
2039 | + return user.company_id.id |
2040 | |
2041 | def _get_companies(self, cr, uid, context=None): |
2042 | c = self._get_company(cr, uid, context) |
2043 | @@ -249,6 +247,9 @@ |
2044 | pass |
2045 | return result |
2046 | |
2047 | + def _get_default_image(self, cr, uid, context=None): |
2048 | + return self.pool['res.partner']._get_default_image(cr, uid, False, colorize=True, context=context) |
2049 | + |
2050 | _defaults = { |
2051 | 'password': '', |
2052 | 'active': True, |
2053 | @@ -256,7 +257,7 @@ |
2054 | 'company_id': _get_company, |
2055 | 'company_ids': _get_companies, |
2056 | 'groups_id': _get_group, |
2057 | - 'image': lambda self, cr, uid, ctx={}: self.pool['res.partner']._get_default_image(cr, uid, False, ctx, colorize=True), |
2058 | + 'image': _get_default_image, |
2059 | } |
2060 | |
2061 | # User can write on a few of his own fields (but not his groups for example) |
2062 | @@ -304,7 +305,8 @@ |
2063 | break |
2064 | else: |
2065 | if 'company_id' in values: |
2066 | - if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']): |
2067 | + user = self.browse(cr, SUPERUSER_ID, uid, context=context) |
2068 | + if not (values['company_id'] in user.company_ids.ids): |
2069 | del values['company_id'] |
2070 | uid = 1 # safe fields only, so we write as super-user to bypass access rights |
2071 | |
2072 | @@ -369,8 +371,8 @@ |
2073 | else: |
2074 | context_key = False |
2075 | if context_key: |
2076 | - res = getattr(user,k) or False |
2077 | - if isinstance(res, browse_record): |
2078 | + res = getattr(user, k) or False |
2079 | + if isinstance(res, BaseModel): |
2080 | res = res.id |
2081 | result[context_key] = res or False |
2082 | return result |
2083 | @@ -392,7 +394,7 @@ |
2084 | if not res: |
2085 | raise openerp.exceptions.AccessDenied() |
2086 | |
2087 | - def login(self, db, login, password): |
2088 | + def _login(self, db, login, password): |
2089 | if not password: |
2090 | return False |
2091 | user_id = False |
2092 | @@ -421,6 +423,7 @@ |
2093 | try: |
2094 | cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False) |
2095 | cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,)) |
2096 | + self.invalidate_cache(cr, user_id, ['login_date'], [user_id]) |
2097 | except Exception: |
2098 | _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True) |
2099 | except openerp.exceptions.AccessDenied: |
2100 | @@ -442,7 +445,7 @@ |
2101 | :param dict user_agent_env: environment dictionary describing any |
2102 | relevant environment attributes |
2103 | """ |
2104 | - uid = self.login(db, login, password) |
2105 | + uid = self._login(db, login, password) |
2106 | if uid == openerp.SUPERUSER_ID: |
2107 | # Successfully logged in as admin! |
2108 | # Attempt to guess the web base url... |
2109 | @@ -670,6 +673,21 @@ |
2110 | (yes if f(x) else nos).append(x) |
2111 | return yes, nos |
2112 | |
2113 | +def parse_m2m(commands): |
2114 | + "return a list of ids corresponding to a many2many value" |
2115 | + ids = [] |
2116 | + for command in commands: |
2117 | + if isinstance(command, (tuple, list)): |
2118 | + if command[0] in (1, 4): |
2119 | + ids.append(command[2]) |
2120 | + elif command[0] == 5: |
2121 | + ids = [] |
2122 | + elif command[0] == 6: |
2123 | + ids = list(command[2]) |
2124 | + else: |
2125 | + ids.append(command) |
2126 | + return ids |
2127 | + |
2128 | |
2129 | class groups_view(osv.osv): |
2130 | _inherit = 'res.groups' |
2131 | @@ -695,7 +713,7 @@ |
2132 | # we have to try-catch this, because at first init the view does not exist |
2133 | # but we are already creating some basic groups |
2134 | view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context) |
2135 | - if view and view.exists() and view._table_name == 'ir.ui.view': |
2136 | + if view and view.exists() and view._name == 'ir.ui.view': |
2137 | xml1, xml2 = [], [] |
2138 | xml1.append(E.separator(string=_('Application'), colspan="4")) |
2139 | for app, kind, gs in self.get_groups_by_application(cr, uid, context): |
2140 | @@ -841,7 +859,7 @@ |
2141 | |
2142 | def _get_reified_groups(self, fields, values): |
2143 | """ compute the given reified group fields from values['groups_id'] """ |
2144 | - gids = set(values.get('groups_id') or []) |
2145 | + gids = set(parse_m2m(values.get('groups_id') or [])) |
2146 | for f in fields: |
2147 | if is_boolean_group(f): |
2148 | values[f] = get_boolean_group(f) in gids |
2149 | |
2150 | === modified file 'openerp/addons/base/security/base_security.xml' |
2151 | --- openerp/addons/base/security/base_security.xml 2014-05-01 21:45:01 +0000 |
2152 | +++ openerp/addons/base/security/base_security.xml 2014-05-19 13:53:27 +0000 |
2153 | @@ -41,12 +41,15 @@ |
2154 | <field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/> |
2155 | </record> |
2156 | |
2157 | +<<<<<<< TREE |
2158 | <!-- Set accesses to menu --> |
2159 | <record model="ir.ui.menu" id="base.menu_administration"> |
2160 | <field name="name">Settings</field> |
2161 | <field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/> |
2162 | </record> |
2163 | |
2164 | +======= |
2165 | +>>>>>>> MERGE-SOURCE |
2166 | <record model="ir.rule" id="res_partner_rule"> |
2167 | <field name="name">res.partner company</field> |
2168 | <field name="model_id" ref="model_res_partner"/> |
2169 | |
2170 | === modified file 'openerp/addons/base/tests/__init__.py' |
2171 | --- openerp/addons/base/tests/__init__.py 2014-02-16 21:44:56 +0000 |
2172 | +++ openerp/addons/base/tests/__init__.py 2014-05-19 13:53:27 +0000 |
2173 | @@ -1,10 +1,9 @@ |
2174 | import test_acl |
2175 | +import test_api |
2176 | import test_base |
2177 | import test_basecase |
2178 | import test_db_cursor |
2179 | import test_expression |
2180 | -import test_expression |
2181 | -import test_fields |
2182 | import test_func |
2183 | import test_ir_actions |
2184 | import test_ir_attachment |
2185 | |
2186 | === modified file 'openerp/addons/base/tests/base_test.yml' |
2187 | --- openerp/addons/base/tests/base_test.yml 2014-05-01 18:42:17 +0000 |
2188 | +++ openerp/addons/base/tests/base_test.yml 2014-05-19 13:53:27 +0000 |
2189 | @@ -277,7 +277,7 @@ |
2190 | rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01', |
2191 | 'rate': value, |
2192 | 'currency_id': currency.id}) |
2193 | - rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate'] |
2194 | + rate = res_currency_rate.read(cr, 1, [rate_id], ['rate'])[0]['rate'] |
2195 | assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected) |
2196 | # res.currency.rate uses 6 digits of precision by default |
2197 | try_roundtrip(2.6748955, 2.674896) |
2198 | |
2199 | === modified file 'openerp/addons/base/tests/test_acl.py' |
2200 | --- openerp/addons/base/tests/test_acl.py 2014-02-09 00:37:45 +0000 |
2201 | +++ openerp/addons/base/tests/test_acl.py 2014-05-19 13:53:27 +0000 |
2202 | @@ -20,6 +20,22 @@ |
2203 | self.tech_group = self.registry('ir.model.data').get_object(self.cr, self.uid, |
2204 | *(GROUP_TECHNICAL_FEATURES.split('.'))) |
2205 | |
2206 | + def _set_field_groups(self, model, field_name, groups): |
2207 | + field = model._fields[field_name] |
2208 | + column = model._columns[field_name] |
2209 | + old_groups = field.groups |
2210 | + old_prefetch = column._prefetch |
2211 | + |
2212 | + field.groups = groups |
2213 | + column.groups = groups |
2214 | + column._prefetch = False |
2215 | + |
2216 | + @self.addCleanup |
2217 | + def cleanup(): |
2218 | + field.groups = old_groups |
2219 | + column.groups = old_groups |
2220 | + column._prefetch = old_prefetch |
2221 | + |
2222 | def test_field_visibility_restriction(self): |
2223 | """Check that model-level ``groups`` parameter effectively restricts access to that |
2224 | field for users who do not belong to one of the explicitly allowed groups""" |
2225 | @@ -33,8 +49,9 @@ |
2226 | self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [], |
2227 | "Field 'accuracy' must be found in view definition before the test") |
2228 | |
2229 | - # Restrict access to the field and check it's gone |
2230 | - self.res_currency._columns['accuracy'].groups = GROUP_TECHNICAL_FEATURES |
2231 | + # restrict access to the field and check it's gone |
2232 | + self._set_field_groups(self.res_currency, 'accuracy', GROUP_TECHNICAL_FEATURES) |
2233 | + |
2234 | fields = self.res_currency.fields_get(self.cr, self.demo_uid, []) |
2235 | form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form') |
2236 | view_arch = etree.fromstring(form_view.get('arch')) |
2237 | @@ -56,7 +73,6 @@ |
2238 | |
2239 | #cleanup |
2240 | self.tech_group.write({'users': [(3, self.demo_uid)]}) |
2241 | - self.res_currency._columns['accuracy'].groups = False |
2242 | |
2243 | @mute_logger('openerp.osv.orm') |
2244 | def test_field_crud_restriction(self): |
2245 | @@ -68,7 +84,8 @@ |
2246 | self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) |
2247 | |
2248 | # Now restrict access to the field and check it's forbidden |
2249 | - self.res_partner._columns['bank_ids'].groups = GROUP_TECHNICAL_FEATURES |
2250 | + self._set_field_groups(self.res_partner, 'bank_ids', GROUP_TECHNICAL_FEATURES) |
2251 | + |
2252 | with self.assertRaises(openerp.osv.orm.except_orm): |
2253 | self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids']) |
2254 | with self.assertRaises(openerp.osv.orm.except_orm): |
2255 | @@ -83,25 +100,22 @@ |
2256 | |
2257 | #cleanup |
2258 | self.tech_group.write({'users': [(3, self.demo_uid)]}) |
2259 | - self.res_partner._columns['bank_ids'].groups = False |
2260 | |
2261 | + @mute_logger('openerp.osv.orm') |
2262 | def test_fields_browse_restriction(self): |
2263 | """Test access to records having restricted fields""" |
2264 | - self.res_partner._columns['email'].groups = GROUP_TECHNICAL_FEATURES |
2265 | - try: |
2266 | - P = self.res_partner |
2267 | - pid = P.search(self.cr, self.demo_uid, [], limit=1)[0] |
2268 | - part = P.browse(self.cr, self.demo_uid, pid) |
2269 | - # accessing fields must no raise exceptions... |
2270 | - part.name |
2271 | - # ... except if they are restricted |
2272 | - with self.assertRaises(openerp.osv.orm.except_orm) as cm: |
2273 | - with mute_logger('openerp.osv.orm'): |
2274 | - part.email |
2275 | - |
2276 | - self.assertEqual(cm.exception.args[0], 'Access Denied') |
2277 | - finally: |
2278 | - self.res_partner._columns['email'].groups = False |
2279 | + self._set_field_groups(self.res_partner, 'email', GROUP_TECHNICAL_FEATURES) |
2280 | + |
2281 | + pid = self.res_partner.search(self.cr, self.demo_uid, [], limit=1)[0] |
2282 | + part = self.res_partner.browse(self.cr, self.demo_uid, pid) |
2283 | + # accessing fields must no raise exceptions... |
2284 | + part.name |
2285 | + # ... except if they are restricted |
2286 | + with self.assertRaises(openerp.osv.orm.except_orm) as cm: |
2287 | + with mute_logger('openerp.osv.orm'): |
2288 | + part.email |
2289 | + |
2290 | + self.assertEqual(cm.exception.args[0], 'AccessError') |
2291 | |
2292 | if __name__ == '__main__': |
2293 | unittest2.main() |
2294 | |
2295 | === added file 'openerp/addons/base/tests/test_api.py' |
2296 | --- openerp/addons/base/tests/test_api.py 1970-01-01 00:00:00 +0000 |
2297 | +++ openerp/addons/base/tests/test_api.py 2014-05-19 13:53:27 +0000 |
2298 | @@ -0,0 +1,442 @@ |
2299 | + |
2300 | +from openerp import BaseModel |
2301 | +from openerp.tools import mute_logger |
2302 | +from openerp.osv.orm import except_orm |
2303 | +from openerp.tests import common |
2304 | + |
2305 | + |
2306 | +class TestAPI(common.TransactionCase): |
2307 | + """ test the new API of the ORM """ |
2308 | + |
2309 | + def assertIsRecordset(self, value, model): |
2310 | + self.assertIsInstance(value, BaseModel) |
2311 | + self.assertEqual(value._name, model) |
2312 | + |
2313 | + def assertIsRecord(self, value, model): |
2314 | + self.assertIsRecordset(value, model) |
2315 | + self.assertTrue(len(value) <= 1) |
2316 | + |
2317 | + def assertIsNull(self, value, model): |
2318 | + self.assertIsRecordset(value, model) |
2319 | + self.assertFalse(value) |
2320 | + |
2321 | + @mute_logger('openerp.osv.orm') |
2322 | + def test_00_query(self): |
2323 | + """ Build a recordset, and check its contents. """ |
2324 | + domain = [('name', 'ilike', 'j')] |
2325 | + ids = self.registry('res.partner').search(self.cr, self.uid, domain) |
2326 | + partners = self.env['res.partner'].search(domain) |
2327 | + |
2328 | + # partners is a collection of browse records corresponding to ids |
2329 | + self.assertTrue(ids) |
2330 | + self.assertTrue(partners) |
2331 | + |
2332 | + # partners and its contents are instance of the model, and share its ormcache |
2333 | + self.assertIsRecordset(partners, 'res.partner') |
2334 | + self.assertIs(partners._ormcache, self.env['res.partner']._ormcache) |
2335 | + for p in partners: |
2336 | + self.assertIsRecord(p, 'res.partner') |
2337 | + self.assertIs(p._ormcache, self.env['res.partner']._ormcache) |
2338 | + |
2339 | + self.assertEqual([p.id for p in partners], ids) |
2340 | + self.assertEqual(self.env['res.partner'].browse(ids), partners) |
2341 | + |
2342 | + @mute_logger('openerp.osv.orm') |
2343 | + def test_01_query_offset(self): |
2344 | + """ Build a recordset with offset, and check equivalence. """ |
2345 | + partners1 = self.env['res.partner'].search([], offset=10) |
2346 | + partners2 = self.env['res.partner'].search([])[10:] |
2347 | + self.assertIsRecordset(partners1, 'res.partner') |
2348 | + self.assertIsRecordset(partners2, 'res.partner') |
2349 | + self.assertEqual(list(partners1), list(partners2)) |
2350 | + |
2351 | + @mute_logger('openerp.osv.orm') |
2352 | + def test_02_query_limit(self): |
2353 | + """ Build a recordset with offset, and check equivalence. """ |
2354 | + partners1 = self.env['res.partner'].search([], limit=10) |
2355 | + partners2 = self.env['res.partner'].search([])[:10] |
2356 | + self.assertIsRecordset(partners1, 'res.partner') |
2357 | + self.assertIsRecordset(partners2, 'res.partner') |
2358 | + self.assertEqual(list(partners1), list(partners2)) |
2359 | + |
2360 | + @mute_logger('openerp.osv.orm') |
2361 | + def test_03_query_offset_limit(self): |
2362 | + """ Build a recordset with offset and limit, and check equivalence. """ |
2363 | + partners1 = self.env['res.partner'].search([], offset=3, limit=7) |
2364 | + partners2 = self.env['res.partner'].search([])[3:10] |
2365 | + self.assertIsRecordset(partners1, 'res.partner') |
2366 | + self.assertIsRecordset(partners2, 'res.partner') |
2367 | + self.assertEqual(list(partners1), list(partners2)) |
2368 | + |
2369 | + @mute_logger('openerp.osv.orm') |
2370 | + def test_05_immutable(self): |
2371 | + """ Check that a recordset remains the same, even after updates. """ |
2372 | + domain = [('name', 'ilike', 'j')] |
2373 | + partners = self.env['res.partner'].search(domain) |
2374 | + self.assertTrue(partners) |
2375 | + ids = map(int, partners) |
2376 | + |
2377 | + # modify those partners, and check that partners has not changed |
2378 | + self.registry('res.partner').write(self.cr, self.uid, ids, {'active': False}) |
2379 | + self.assertEqual(ids, map(int, partners)) |
2380 | + |
2381 | + # redo the search, and check that the result is now empty |
2382 | + partners2 = self.env['res.partner'].search(domain) |
2383 | + self.assertFalse(partners2) |
2384 | + |
2385 | + @mute_logger('openerp.osv.orm') |
2386 | + def test_06_fields(self): |
2387 | + """ Check that relation fields return records, recordsets or nulls. """ |
2388 | + user = self.registry('res.users').browse(self.cr, self.uid, self.uid) |
2389 | + self.assertIsRecord(user, 'res.users') |
2390 | + self.assertIsRecord(user.partner_id, 'res.partner') |
2391 | + self.assertIsRecordset(user.groups_id, 'res.groups') |
2392 | + |
2393 | + partners = self.env['res.partner'].search([]) |
2394 | + for name, cinfo in partners._all_columns.iteritems(): |
2395 | + if cinfo.column._type == 'many2one': |
2396 | + for p in partners: |
2397 | + self.assertIsRecord(p[name], cinfo.column._obj) |
2398 | + elif cinfo.column._type == 'reference': |
2399 | + for p in partners: |
2400 | + if p[name]: |
2401 | + self.assertIsRecord(p[name], cinfo.column._obj) |
2402 | + elif cinfo.column._type in ('one2many', 'many2many'): |
2403 | + for p in partners: |
2404 | + self.assertIsRecordset(p[name], cinfo.column._obj) |
2405 | + |
2406 | + @mute_logger('openerp.osv.orm') |
2407 | + def test_07_null(self): |
2408 | + """ Check behavior of null instances. """ |
2409 | + # select a partner without a parent |
2410 | + partner = self.env['res.partner'].search([('parent_id', '=', False)])[0] |
2411 | + |
2412 | + # check partner and related null instances |
2413 | + self.assertTrue(partner) |
2414 | + self.assertIsRecord(partner, 'res.partner') |
2415 | + |
2416 | + self.assertFalse(partner.parent_id) |
2417 | + self.assertIsNull(partner.parent_id, 'res.partner') |
2418 | + |
2419 | + self.assertIs(partner.parent_id.id, False) |
2420 | + |
2421 | + self.assertFalse(partner.parent_id.user_id) |
2422 | + self.assertIsNull(partner.parent_id.user_id, 'res.users') |
2423 | + |
2424 | + self.assertIs(partner.parent_id.user_id.name, False) |
2425 | + |
2426 | + self.assertFalse(partner.parent_id.user_id.groups_id) |
2427 | + self.assertIsRecordset(partner.parent_id.user_id.groups_id, 'res.groups') |
2428 | + |
2429 | + @mute_logger('openerp.osv.orm') |
2430 | + def test_10_old_old(self): |
2431 | + """ Call old-style methods in the old-fashioned way. """ |
2432 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2433 | + self.assertTrue(partners) |
2434 | + ids = map(int, partners) |
2435 | + |
2436 | + # call method name_get on partners' model, and check its effect |
2437 | + res = partners._model.name_get(self.cr, self.uid, ids) |
2438 | + self.assertEqual(len(res), len(ids)) |
2439 | + self.assertEqual(set(val[0] for val in res), set(ids)) |
2440 | + |
2441 | + @mute_logger('openerp.osv.orm') |
2442 | + def test_20_old_new(self): |
2443 | + """ Call old-style methods in the new API style. """ |
2444 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2445 | + self.assertTrue(partners) |
2446 | + |
2447 | + # call method name_get on partners itself, and check its effect |
2448 | + res = partners.name_get() |
2449 | + self.assertEqual(len(res), len(partners)) |
2450 | + self.assertEqual(set(val[0] for val in res), set(map(int, partners))) |
2451 | + |
2452 | + @mute_logger('openerp.osv.orm') |
2453 | + def test_25_old_new(self): |
2454 | + """ Call old-style methods on records (new API style). """ |
2455 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2456 | + self.assertTrue(partners) |
2457 | + |
2458 | + # call method name_get on partner records, and check its effect |
2459 | + for p in partners: |
2460 | + res = p.name_get() |
2461 | + self.assertTrue(isinstance(res, list) and len(res) == 1) |
2462 | + self.assertTrue(isinstance(res[0], tuple) and len(res[0]) == 2) |
2463 | + self.assertEqual(res[0][0], p.id) |
2464 | + |
2465 | + @mute_logger('openerp.osv.orm') |
2466 | + def test_30_new_old(self): |
2467 | + """ Call new-style methods in the old-fashioned way. """ |
2468 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2469 | + self.assertTrue(partners) |
2470 | + ids = map(int, partners) |
2471 | + |
2472 | + # call method write on partners' model, and check its effect |
2473 | + partners._model.write(self.cr, self.uid, ids, {'active': False}) |
2474 | + for p in partners: |
2475 | + self.assertFalse(p.active) |
2476 | + |
2477 | + @mute_logger('openerp.osv.orm') |
2478 | + def test_40_new_new(self): |
2479 | + """ Call new-style methods in the new API style. """ |
2480 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2481 | + self.assertTrue(partners) |
2482 | + |
2483 | + # call method write on partners itself, and check its effect |
2484 | + partners.write({'active': False}) |
2485 | + for p in partners: |
2486 | + self.assertFalse(p.active) |
2487 | + |
2488 | + @mute_logger('openerp.osv.orm') |
2489 | + def test_45_new_new(self): |
2490 | + """ Call new-style methods on records (new API style). """ |
2491 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2492 | + self.assertTrue(partners) |
2493 | + |
2494 | + # call method write on partner records, and check its effects |
2495 | + for p in partners: |
2496 | + p.write({'active': False}) |
2497 | + for p in partners: |
2498 | + self.assertFalse(p.active) |
2499 | + |
2500 | + @mute_logger('openerp.osv.orm') |
2501 | + @mute_logger('openerp.addons.base.ir.ir_model') |
2502 | + def test_50_environment(self): |
2503 | + """ Test environment on records. """ |
2504 | + # partners and reachable records are attached to self.env |
2505 | + partners = self.env['res.partner'].search([('name', 'ilike', 'j')]) |
2506 | + self.assertEqual(partners.env, self.env) |
2507 | + for x in (partners, partners[0], partners[0].company_id): |
2508 | + self.assertEqual(x.env, self.env) |
2509 | + for p in partners: |
2510 | + self.assertEqual(p.env, self.env) |
2511 | + |
2512 | + # check that the current user can read and modify company data |
2513 | + partners[0].company_id.name |
2514 | + partners[0].company_id.write({'name': 'Fools'}) |
2515 | + |
2516 | + # create an environment with the demo user |
2517 | + demo = self.env['res.users'].search([('login', '=', 'demo')])[0] |
2518 | + demo_env = self.env(user=demo) |
2519 | + self.assertNotEqual(demo_env, self.env) |
2520 | + |
2521 | + # partners and related records are still attached to self.env |
2522 | + self.assertEqual(partners.env, self.env) |
2523 | + for x in (partners, partners[0], partners[0].company_id): |
2524 | + self.assertEqual(x.env, self.env) |
2525 | + for p in partners: |
2526 | + self.assertEqual(p.env, self.env) |
2527 | + |
2528 | + # create record instances attached to demo_env |
2529 | + demo_partners = partners.sudo(user=demo) |
2530 | + self.assertEqual(demo_partners.env, demo_env) |
2531 | + for x in (demo_partners, demo_partners[0], demo_partners[0].company_id): |
2532 | + self.assertEqual(x.env, demo_env) |
2533 | + for p in demo_partners: |
2534 | + self.assertEqual(p.env, demo_env) |
2535 | + |
2536 | + # demo user can read but not modify company data |
2537 | + demo_partners[0].company_id.name |
2538 | + with self.assertRaises(except_orm): |
2539 | + demo_partners[0].company_id.write({'name': 'Pricks'}) |
2540 | + |
2541 | + # remove demo user from all groups |
2542 | + demo.write({'groups_id': [(5,)]}) |
2543 | + |
2544 | + # demo user can no longer access partner data |
2545 | + with self.assertRaises(except_orm): |
2546 | + demo_partners[0].company_id.name |
2547 | + |
2548 | + @mute_logger('openerp.osv.orm') |
2549 | + def test_55_draft(self): |
2550 | + """ Test draft mode nesting. """ |
2551 | + env = self.env |
2552 | + self.assertFalse(env.draft) |
2553 | + with env.do_in_draft(): |
2554 | + self.assertTrue(env.draft) |
2555 | + with env.do_in_draft(): |
2556 | + self.assertTrue(env.draft) |
2557 | + with env.do_in_draft(): |
2558 | + self.assertTrue(env.draft) |
2559 | + self.assertTrue(env.draft) |
2560 | + self.assertTrue(env.draft) |
2561 | + self.assertFalse(env.draft) |
2562 | + |
2563 | + @mute_logger('openerp.osv.orm') |
2564 | + def test_60_cache(self): |
2565 | + """ Check the record cache behavior """ |
2566 | + partners = self.env['res.partner'].search([('child_ids', '!=', False)]) |
2567 | + partner1, partner2 = partners[0], partners[1] |
2568 | + children1, children2 = partner1.child_ids, partner2.child_ids |
2569 | + self.assertTrue(children1) |
2570 | + self.assertTrue(children2) |
2571 | + |
2572 | + # take a child contact |
2573 | + child = children1[0] |
2574 | + self.assertEqual(child.parent_id, partner1) |
2575 | + self.assertIn(child, partner1.child_ids) |
2576 | + self.assertNotIn(child, partner2.child_ids) |
2577 | + |
2578 | + # fetch data in the cache |
2579 | + for p in partners: |
2580 | + p.name, p.company_id.name, p.user_id.name, p.contact_address |
2581 | + self.env.check_cache() |
2582 | + |
2583 | + # change its parent |
2584 | + child.write({'parent_id': partner2.id}) |
2585 | + self.env.check_cache() |
2586 | + |
2587 | + # check recordsets |
2588 | + self.assertEqual(child.parent_id, partner2) |
2589 | + self.assertNotIn(child, partner1.child_ids) |
2590 | + self.assertIn(child, partner2.child_ids) |
2591 | + self.assertEqual(set(partner1.child_ids + child), set(children1)) |
2592 | + self.assertEqual(set(partner2.child_ids), set(children2 + child)) |
2593 | + self.env.check_cache() |
2594 | + |
2595 | + # delete it |
2596 | + child.unlink() |
2597 | + self.env.check_cache() |
2598 | + |
2599 | + # check recordsets |
2600 | + self.assertEqual(set(partner1.child_ids), set(children1) - set([child])) |
2601 | + self.assertEqual(set(partner2.child_ids), set(children2)) |
2602 | + self.env.check_cache() |
2603 | + |
2604 | + @mute_logger('openerp.osv.orm') |
2605 | + def test_60_cache_prefetching(self): |
2606 | + """ Check the record cache prefetching """ |
2607 | + self.env.invalidate_all() |
2608 | + |
2609 | + # all the records of an instance already have an entry in cache |
2610 | + partners = self.env['res.partner'].search([]) |
2611 | + partner_ids = self.env.prefetch['res.partner'] |
2612 | + self.assertEqual(set(partners.ids), set(partner_ids)) |
2613 | + |
2614 | + # countries have not been fetched yet; their cache must be empty |
2615 | + countries = self.env['res.country'].browse() |
2616 | + self.assertFalse(self.env.prefetch['res.country']) |
2617 | + |
2618 | + # reading ONE partner should fetch them ALL |
2619 | + countries |= partners[0].country_id |
2620 | + country_cache = self.env.cache[partners._fields['country_id']] |
2621 | + self.assertLessEqual(set(partners._ids), set(country_cache)) |
2622 | + |
2623 | + # read all partners, and check that the cache already contained them |
2624 | + country_ids = list(self.env.prefetch['res.country']) |
2625 | + for p in partners: |
2626 | + countries |= p.country_id |
2627 | + self.assertLessEqual(set(countries.ids), set(country_ids)) |
2628 | + |
2629 | + @mute_logger('openerp.osv.orm') |
2630 | + def test_70_one(self): |
2631 | + """ Check method one(). """ |
2632 | + # check with many records |
2633 | + ps = self.env['res.partner'].search([('name', 'ilike', 'a')]) |
2634 | + self.assertTrue(len(ps) > 1) |
2635 | + with self.assertRaises(except_orm): ps.one() |
2636 | + |
2637 | + p1 = ps[0] |
2638 | + self.assertEqual(len(p1), 1) |
2639 | + self.assertEqual(p1.one(), p1) |
2640 | + |
2641 | + p0 = self.env['res.partner'].browse() |
2642 | + self.assertEqual(len(p0), 0) |
2643 | + with self.assertRaises(except_orm): p0.one() |
2644 | + |
2645 | + @mute_logger('openerp.osv.orm') |
2646 | + def test_80_contains(self): |
2647 | + """ Test membership on recordset. """ |
2648 | + p1 = self.env['res.partner'].search([('name', 'ilike', 'a')], limit=1).one() |
2649 | + ps = self.env['res.partner'].search([('name', 'ilike', 'a')]) |
2650 | + self.assertTrue(p1 in ps) |
2651 | + |
2652 | + @mute_logger('openerp.osv.orm') |
2653 | + def test_80_set_operations(self): |
2654 | + """ Check set operations on recordsets. """ |
2655 | + pa = self.env['res.partner'].search([('name', 'ilike', 'a')]) |
2656 | + pb = self.env['res.partner'].search([('name', 'ilike', 'b')]) |
2657 | + self.assertTrue(pa) |
2658 | + self.assertTrue(pb) |
2659 | + self.assertTrue(set(pa) & set(pb)) |
2660 | + |
2661 | + concat = pa + pb |
2662 | + self.assertEqual(list(concat), list(pa) + list(pb)) |
2663 | + self.assertEqual(len(concat), len(pa) + len(pb)) |
2664 | + |
2665 | + difference = pa - pb |
2666 | + self.assertEqual(len(difference), len(set(difference))) |
2667 | + self.assertEqual(set(difference), set(pa) - set(pb)) |
2668 | + self.assertLessEqual(difference, pa) |
2669 | + |
2670 | + intersection = pa & pb |
2671 | + self.assertEqual(len(intersection), len(set(intersection))) |
2672 | + self.assertEqual(set(intersection), set(pa) & set(pb)) |
2673 | + self.assertLessEqual(intersection, pa) |
2674 | + self.assertLessEqual(intersection, pb) |
2675 | + |
2676 | + union = pa | pb |
2677 | + self.assertEqual(len(union), len(set(union))) |
2678 | + self.assertEqual(set(union), set(pa) | set(pb)) |
2679 | + self.assertGreaterEqual(union, pa) |
2680 | + self.assertGreaterEqual(union, pb) |
2681 | + |
2682 | + # one cannot mix different models with set operations |
2683 | + ps = pa |
2684 | + ms = self.env['ir.ui.menu'].search([]) |
2685 | + self.assertNotEqual(ps._name, ms._name) |
2686 | + self.assertNotEqual(ps, ms) |
2687 | + |
2688 | + with self.assertRaises(except_orm): |
2689 | + res = ps + ms |
2690 | + with self.assertRaises(except_orm): |
2691 | + res = ps - ms |
2692 | + with self.assertRaises(except_orm): |
2693 | + res = ps & ms |
2694 | + with self.assertRaises(except_orm): |
2695 | + res = ps | ms |
2696 | + with self.assertRaises(except_orm): |
2697 | + res = ps < ms |
2698 | + with self.assertRaises(except_orm): |
2699 | + res = ps <= ms |
2700 | + with self.assertRaises(except_orm): |
2701 | + res = ps > ms |
2702 | + with self.assertRaises(except_orm): |
2703 | + res = ps >= ms |
2704 | + |
2705 | + @mute_logger('openerp.osv.orm') |
2706 | + def test_80_filter(self): |
2707 | + """ Check filter on recordsets. """ |
2708 | + ps = self.env['res.partner'].search([]) |
2709 | + customers = ps.browse([p.id for p in ps if p.customer]) |
2710 | + |
2711 | + # filter on a single field |
2712 | + self.assertEqual(ps.filter(lambda p: p.customer), customers) |
2713 | + self.assertEqual(ps.filter('customer'), customers) |
2714 | + |
2715 | + # filter on a sequence of fields |
2716 | + self.assertEqual( |
2717 | + ps.filter(lambda p: p.parent_id.customer), |
2718 | + ps.filter('parent_id.customer') |
2719 | + ) |
2720 | + |
2721 | + @mute_logger('openerp.osv.orm') |
2722 | + def test_80_map(self): |
2723 | + """ Check map on recordsets. """ |
2724 | + ps = self.env['res.partner'].search([]) |
2725 | + parents = ps.browse() |
2726 | + for p in ps: parents |= p.parent_id |
2727 | + |
2728 | + # map a single field |
2729 | + self.assertEqual(ps.map(lambda p: p.parent_id), parents) |
2730 | + self.assertEqual(ps.map('parent_id'), parents) |
2731 | + |
2732 | + # map a sequence of fields |
2733 | + self.assertEqual( |
2734 | + ps.map(lambda p: p.parent_id.name), |
2735 | + [p.parent_id.name for p in ps] |
2736 | + ) |
2737 | + self.assertEqual( |
2738 | + ps.map('parent_id.name'), |
2739 | + [p.name for p in parents] |
2740 | + ) |
2741 | |
2742 | === modified file 'openerp/addons/base/tests/test_ir_rule.yml' |
2743 | --- openerp/addons/base/tests/test_ir_rule.yml 2014-05-01 18:42:17 +0000 |
2744 | +++ openerp/addons/base/tests/test_ir_rule.yml 2014-05-19 13:53:27 +0000 |
2745 | @@ -124,7 +124,7 @@ |
2746 | Modify the global rule on res_company which triggers a recursive check |
2747 | of the rules on company. |
2748 | - |
2749 | - !record {model: ir.rule, id: base.res_company_rule}: |
2750 | + !record {model: ir.rule, id: res_company_rule}: |
2751 | domain_force: "[('id','child_of',[user.company_id.id])]" |
2752 | - |
2753 | Read as demo user the partners (exercising the global company rule). |
2754 | |
2755 | === modified file 'openerp/addons/base/tests/test_orm.py' |
2756 | --- openerp/addons/base/tests/test_orm.py 2014-04-07 09:10:28 +0000 |
2757 | +++ openerp/addons/base/tests/test_orm.py 2014-05-19 13:53:27 +0000 |
2758 | @@ -77,6 +77,16 @@ |
2759 | with self.assertRaises(Exception): |
2760 | self.partner.unlink(cr, uid2, [p1,p2]) |
2761 | |
2762 | + def test_multi_read(self): |
2763 | + record_id = self.partner.create(self.cr, UID, {'name': 'MyPartner1'}) |
2764 | + records = self.partner.read(self.cr, UID, [record_id]) |
2765 | + self.assertIsInstance(records, list) |
2766 | + |
2767 | + def test_one_read(self): |
2768 | + record_id = self.partner.create(self.cr, UID, {'name': 'MyPartner1'}) |
2769 | + record = self.partner.read(self.cr, UID, record_id) |
2770 | + self.assertIsInstance(record, dict) |
2771 | + |
2772 | @mute_logger('openerp.osv.orm') |
2773 | def test_search_read(self): |
2774 | # simple search_read |
2775 | @@ -194,8 +204,10 @@ |
2776 | """ copying a user should automatically copy its partner, too """ |
2777 | foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) |
2778 | foo_before, = self.user.read(self.cr, UID, [foo_id]) |
2779 | + del foo_before['__last_update'] |
2780 | bar_id = self.user.copy(self.cr, UID, foo_id, {'login': 'bar', 'password': 'bar'}) |
2781 | foo_after, = self.user.read(self.cr, UID, [foo_id]) |
2782 | + del foo_after['__last_update'] |
2783 | |
2784 | self.assertEqual(foo_before, foo_after) |
2785 | |
2786 | @@ -211,9 +223,11 @@ |
2787 | par_id = self.partner.create(self.cr, UID, {'name': 'Bar'}) |
2788 | |
2789 | foo_before, = self.user.read(self.cr, UID, [foo_id]) |
2790 | + del foo_before['__last_update'] |
2791 | partners_before = self.partner.search(self.cr, UID, []) |
2792 | bar_id = self.user.copy(self.cr, UID, foo_id, {'partner_id': par_id, 'login': 'bar'}) |
2793 | foo_after, = self.user.read(self.cr, UID, [foo_id]) |
2794 | + del foo_after['__last_update'] |
2795 | partners_after = self.partner.search(self.cr, UID, []) |
2796 | |
2797 | self.assertEqual(foo_before, foo_after) |
2798 | |
2799 | === modified file 'openerp/addons/base/tests/test_osv_expression.yml' |
2800 | --- openerp/addons/base/tests/test_osv_expression.yml 2014-05-01 18:42:17 +0000 |
2801 | +++ openerp/addons/base/tests/test_osv_expression.yml 2014-05-19 13:53:27 +0000 |
2802 | @@ -83,7 +83,7 @@ |
2803 | Test one2many operator with False |
2804 | - |
2805 | !assert {model: res.partner, search: "[('child_ids', '=', False)]"}: |
2806 | - - child_ids in (False, None, []) |
2807 | + - list(child_ids) == [] |
2808 | - |
2809 | Test many2many operator with empty search list |
2810 | - |
2811 | @@ -92,7 +92,7 @@ |
2812 | Test many2many operator with False |
2813 | - |
2814 | !assert {model: res.partner, search: "[('category_id', '=', False)]"}: |
2815 | - - category_id in (False, None, []) |
2816 | + - list(category_id) == [] |
2817 | - |
2818 | Filtering on invalid value across x2many relationship should return an empty set |
2819 | - |
2820 | |
2821 | === modified file 'openerp/addons/base/tests/test_views.py' |
2822 | --- openerp/addons/base/tests/test_views.py 2014-04-08 11:49:36 +0000 |
2823 | +++ openerp/addons/base/tests/test_views.py 2014-05-19 13:53:27 +0000 |
2824 | @@ -7,6 +7,7 @@ |
2825 | from lxml.builder import E |
2826 | |
2827 | from openerp.tests import common |
2828 | +from openerp.tools import mute_logger |
2829 | |
2830 | Field = E.field |
2831 | |
2832 | @@ -340,6 +341,7 @@ |
2833 | name="target"), |
2834 | string="Title")) |
2835 | |
2836 | + @mute_logger('openerp.addons.base.ir.ir_ui_view') |
2837 | def test_invalid_position(self): |
2838 | spec = Field( |
2839 | Field(name="whoops"), |
2840 | @@ -350,6 +352,7 @@ |
2841 | self.base_arch, |
2842 | spec, None) |
2843 | |
2844 | + @mute_logger('openerp.addons.base.ir.ir_ui_view') |
2845 | def test_incorrect_version(self): |
2846 | # Version ignored on //field elements, so use something else |
2847 | arch = E.form(E.element(foo="42")) |
2848 | @@ -362,6 +365,7 @@ |
2849 | arch, |
2850 | spec, None) |
2851 | |
2852 | + @mute_logger('openerp.addons.base.ir.ir_ui_view') |
2853 | def test_target_not_found(self): |
2854 | spec = Field(name="targut") |
2855 | |
2856 | |
2857 | === modified file 'openerp/addons/test_impex/models.py' |
2858 | --- openerp/addons/test_impex/models.py 2013-02-11 14:36:47 +0000 |
2859 | +++ openerp/addons/test_impex/models.py 2014-05-19 13:53:27 +0000 |
2860 | @@ -6,6 +6,7 @@ |
2861 | |
2862 | def function_fn(model, cr, uid, ids, field_name, arg, context): |
2863 | return dict((id, 3) for id in ids) |
2864 | + |
2865 | def function_fn_write(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context): |
2866 | """ just so CreatorCase.export can be used |
2867 | """ |
2868 | @@ -23,7 +24,8 @@ |
2869 | ('datetime', fields.datetime()), |
2870 | ('text', fields.text()), |
2871 | ('selection', fields.selection([(1, "Foo"), (2, "Bar"), (3, "Qux"), (4, '')])), |
2872 | - ('selection.function', fields.selection(selection_fn)), |
2873 | + # here use size=-1 to store the values as integers instead of strings |
2874 | + ('selection.function', fields.selection(selection_fn, size=-1)), |
2875 | # just relate to an integer |
2876 | ('many2one', fields.many2one('export.integer')), |
2877 | ('one2many', fields.one2many('export.one2many.child', 'parent_id')), |
2878 | @@ -32,28 +34,29 @@ |
2879 | # related: specialization of fields.function, should work the same way |
2880 | # TODO: reference |
2881 | ] |
2882 | + |
2883 | for name, field in models: |
2884 | - attrs = { |
2885 | - '_name': 'export.%s' % name, |
2886 | - '_columns': { |
2887 | + class NewModel(orm.Model): |
2888 | + _name = 'export.%s' % name |
2889 | + _columns = { |
2890 | 'const': fields.integer(), |
2891 | - 'value': field |
2892 | - }, |
2893 | - '_defaults': {'const': 4}, |
2894 | - 'name_get': (lambda self, cr, uid, ids, context=None: |
2895 | - [(record.id, "%s:%s" % (self._name, record.value)) |
2896 | - for record in self.browse(cr, uid, ids, context=context)]), |
2897 | - 'name_search': (lambda self, cr, uid, name, operator, context=None: |
2898 | - self.name_get(cr, uid, |
2899 | - self.search(cr, uid, [['value', operator, int(name.split(':')[1])]]) |
2900 | - , context=context) |
2901 | - if isinstance(name, basestring) and name.split(':')[0] == self._name |
2902 | - else []) |
2903 | - } |
2904 | - NewModel = type( |
2905 | - 'Export%s' % ''.join(section.capitalize() for section in name.split('.')), |
2906 | - (orm.Model,), |
2907 | - attrs) |
2908 | + 'value': field, |
2909 | + } |
2910 | + _defaults = { |
2911 | + 'const': 4, |
2912 | + } |
2913 | + |
2914 | + def name_get(self, cr, uid, ids, context=None): |
2915 | + return [(record.id, "%s:%s" % (self._name, record.value)) |
2916 | + for record in self.browse(cr, uid, ids, context=context)] |
2917 | + |
2918 | + def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): |
2919 | + if isinstance(name, basestring) and name.split(':')[0] == self._name: |
2920 | + ids = self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) |
2921 | + return self.name_get(cr, user, ids, context=context) |
2922 | + else: |
2923 | + return [] |
2924 | + |
2925 | |
2926 | class One2ManyChild(orm.Model): |
2927 | _name = 'export.one2many.child' |
2928 | @@ -63,28 +66,33 @@ |
2929 | _columns = { |
2930 | 'parent_id': fields.many2one('export.one2many'), |
2931 | 'str': fields.char('unknown', size=None), |
2932 | - 'value': fields.integer() |
2933 | + 'value': fields.integer(), |
2934 | } |
2935 | + |
2936 | def name_get(self, cr, uid, ids, context=None): |
2937 | return [(record.id, "%s:%s" % (self._name, record.value)) |
2938 | for record in self.browse(cr, uid, ids, context=context)] |
2939 | + |
2940 | def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): |
2941 | - return (self.name_get(cr, user, |
2942 | - self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) |
2943 | - , context=context) |
2944 | - if isinstance(name, basestring) and name.split(':')[0] == self._name |
2945 | - else []) |
2946 | + if isinstance(name, basestring) and name.split(':')[0] == self._name: |
2947 | + ids = self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) |
2948 | + return self.name_get(cr, user, ids, context=context) |
2949 | + else: |
2950 | + return [] |
2951 | + |
2952 | |
2953 | class One2ManyMultiple(orm.Model): |
2954 | _name = 'export.one2many.multiple' |
2955 | - |
2956 | _columns = { |
2957 | 'parent_id': fields.many2one('export.one2many.recursive'), |
2958 | 'const': fields.integer(), |
2959 | 'child1': fields.one2many('export.one2many.child.1', 'parent_id'), |
2960 | 'child2': fields.one2many('export.one2many.child.2', 'parent_id'), |
2961 | } |
2962 | - _defaults = { 'const': 36 } |
2963 | + _defaults = { |
2964 | + 'const': 36, |
2965 | + } |
2966 | + |
2967 | |
2968 | class One2ManyChildMultiple(orm.Model): |
2969 | _name = 'export.one2many.multiple.child' |
2970 | @@ -94,18 +102,24 @@ |
2971 | _columns = { |
2972 | 'parent_id': fields.many2one('export.one2many.multiple'), |
2973 | 'str': fields.char('unknown', size=None), |
2974 | - 'value': fields.integer() |
2975 | + 'value': fields.integer(), |
2976 | } |
2977 | + |
2978 | def name_get(self, cr, uid, ids, context=None): |
2979 | return [(record.id, "%s:%s" % (self._name, record.value)) |
2980 | for record in self.browse(cr, uid, ids, context=context)] |
2981 | + |
2982 | + |
2983 | class One2ManyChild1(orm.Model): |
2984 | _name = 'export.one2many.child.1' |
2985 | _inherit = 'export.one2many.multiple.child' |
2986 | + |
2987 | + |
2988 | class One2ManyChild2(orm.Model): |
2989 | _name = 'export.one2many.child.2' |
2990 | _inherit = 'export.one2many.multiple.child' |
2991 | |
2992 | + |
2993 | class Many2ManyChild(orm.Model): |
2994 | _name = 'export.many2many.other' |
2995 | # FIXME: orm.py:1161, fix to name_get on m2o field |
2996 | @@ -113,21 +127,23 @@ |
2997 | |
2998 | _columns = { |
2999 | 'str': fields.char('unknown', size=None), |
3000 | - 'value': fields.integer() |
3001 | + 'value': fields.integer(), |
3002 | } |
3003 | + |
3004 | def name_get(self, cr, uid, ids, context=None): |
3005 | return [(record.id, "%s:%s" % (self._name, record.value)) |
3006 | for record in self.browse(cr, uid, ids, context=context)] |
3007 | + |
3008 | def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): |
3009 | - return (self.name_get(cr, user, |
3010 | - self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) |
3011 | - , context=context) |
3012 | - if isinstance(name, basestring) and name.split(':')[0] == self._name |
3013 | - else []) |
3014 | + if isinstance(name, basestring) and name.split(':')[0] == self._name: |
3015 | + ids = self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) |
3016 | + return self.name_get(cr, user, ids, context=context) |
3017 | + else: |
3018 | + return [] |
3019 | + |
3020 | |
3021 | class SelectionWithDefault(orm.Model): |
3022 | _name = 'export.selection.withdefault' |
3023 | - |
3024 | _columns = { |
3025 | 'const': fields.integer(), |
3026 | 'value': fields.selection([(1, "Foo"), (2, "Bar")]), |
3027 | @@ -137,12 +153,12 @@ |
3028 | 'value': 2, |
3029 | } |
3030 | |
3031 | + |
3032 | class RecO2M(orm.Model): |
3033 | _name = 'export.one2many.recursive' |
3034 | - |
3035 | _columns = { |
3036 | 'value': fields.integer(), |
3037 | - 'child': fields.one2many('export.one2many.multiple', 'parent_id') |
3038 | + 'child': fields.one2many('export.one2many.multiple', 'parent_id'), |
3039 | } |
3040 | |
3041 | class OnlyOne(orm.Model): |
3042 | |
3043 | === modified file 'openerp/addons/test_impex/tests/test_export.py' |
3044 | --- openerp/addons/test_impex/tests/test_export.py 2013-10-24 21:47:35 +0000 |
3045 | +++ openerp/addons/test_impex/tests/test_export.py 2014-05-19 13:53:27 +0000 |
3046 | @@ -16,15 +16,14 @@ |
3047 | def setUp(self): |
3048 | super(CreatorCase, self).setUp() |
3049 | self.model = self.registry(self.model_name) |
3050 | + |
3051 | def make(self, value): |
3052 | id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': value}) |
3053 | return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0] |
3054 | + |
3055 | def export(self, value, fields=('value',), context=None): |
3056 | record = self.make(value) |
3057 | - return self.model._BaseModel__export_row( |
3058 | - self.cr, openerp.SUPERUSER_ID, record, |
3059 | - [f.split('/') for f in fields], |
3060 | - context=context) |
3061 | + return record._BaseModel__export_rows([f.split('/') for f in fields]) |
3062 | |
3063 | class test_boolean_field(CreatorCase): |
3064 | model_name = 'export.boolean' |
3065 | @@ -272,10 +271,10 @@ |
3066 | # FIXME: selection functions export the *value* itself |
3067 | self.assertEqual( |
3068 | self.export(1), |
3069 | - [[u'1']]) |
3070 | + [[1]]) |
3071 | self.assertEqual( |
3072 | self.export(3), |
3073 | - [[u'3']]) |
3074 | + [[3]]) |
3075 | # fucking hell |
3076 | self.assertEqual( |
3077 | self.export(0), |
3078 | @@ -433,12 +432,10 @@ |
3079 | if value is not None: values['value'] = value |
3080 | id = self.model.create(self.cr, openerp.SUPERUSER_ID, values) |
3081 | return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0] |
3082 | + |
3083 | def export(self, value=None, fields=('child1', 'child2',), context=None, **values): |
3084 | record = self.make(value, **values) |
3085 | - return self.model._BaseModel__export_row( |
3086 | - self.cr, openerp.SUPERUSER_ID, record, |
3087 | - [f.split('/') for f in fields], |
3088 | - context=context) |
3089 | + return record._BaseModel__export_rows([f.split('/') for f in fields]) |
3090 | |
3091 | def test_empty(self): |
3092 | self.assertEqual( |
3093 | |
3094 | === modified file 'openerp/addons/test_impex/tests/test_import.py' |
3095 | --- openerp/addons/test_impex/tests/test_import.py 2012-10-10 15:44:36 +0000 |
3096 | +++ openerp/addons/test_impex/tests/test_import.py 2014-05-19 13:53:27 +0000 |
3097 | @@ -57,7 +57,7 @@ |
3098 | |
3099 | ids = ModelData.search( |
3100 | self.cr, openerp.SUPERUSER_ID, |
3101 | - [('model', '=', record._table_name), ('res_id', '=', record.id)]) |
3102 | + [('model', '=', record._name), ('res_id', '=', record.id)]) |
3103 | if ids: |
3104 | d = ModelData.read( |
3105 | self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0] |
3106 | @@ -65,12 +65,12 @@ |
3107 | return '%s.%s' % (d['module'], d['name']) |
3108 | return d['name'] |
3109 | |
3110 | - name = dict(record.name_get())[record.id] |
3111 | + name = record.name_get()[0][1] |
3112 | # fix dotted name_get results, otherwise xid lookups blow up |
3113 | name = name.replace('.', '-') |
3114 | ModelData.create(self.cr, openerp.SUPERUSER_ID, { |
3115 | 'name': name, |
3116 | - 'model': record._table_name, |
3117 | + 'model': record._name, |
3118 | 'res_id': record.id, |
3119 | 'module': '__test__' |
3120 | }) |
3121 | @@ -446,7 +446,7 @@ |
3122 | ]), |
3123 | ok(2)) |
3124 | self.assertEqual( |
3125 | - ['3', '1'], |
3126 | + [3, 1], |
3127 | values(self.read())) |
3128 | |
3129 | def test_translated(self): |
3130 | @@ -661,7 +661,7 @@ |
3131 | id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) |
3132 | records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) |
3133 | |
3134 | - name = lambda record: dict(record.name_get())[record.id] |
3135 | + name = lambda record: record.name_get()[0][1] |
3136 | |
3137 | self.assertEqual( |
3138 | self.import_(['value'], [ |
3139 | |
3140 | === modified file 'openerp/addons/test_impex/tests/test_load.py' |
3141 | --- openerp/addons/test_impex/tests/test_load.py 2013-02-25 10:55:23 +0000 |
3142 | +++ openerp/addons/test_impex/tests/test_load.py 2014-05-19 13:53:27 +0000 |
3143 | @@ -56,7 +56,7 @@ |
3144 | |
3145 | ids = ModelData.search( |
3146 | self.cr, openerp.SUPERUSER_ID, |
3147 | - [('model', '=', record._table_name), ('res_id', '=', record.id)]) |
3148 | + [('model', '=', record._name), ('res_id', '=', record.id)]) |
3149 | if ids: |
3150 | d = ModelData.read( |
3151 | self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0] |
3152 | @@ -64,12 +64,12 @@ |
3153 | return '%s.%s' % (d['module'], d['name']) |
3154 | return d['name'] |
3155 | |
3156 | - name = dict(record.name_get())[record.id] |
3157 | + name = record.name_get()[0][1] |
3158 | # fix dotted name_get results, otherwise xid lookups blow up |
3159 | name = name.replace('.', '-') |
3160 | ModelData.create(self.cr, openerp.SUPERUSER_ID, { |
3161 | 'name': name, |
3162 | - 'model': record._table_name, |
3163 | + 'model': record._name, |
3164 | 'res_id': record.id, |
3165 | 'module': '__test__' |
3166 | }) |
3167 | @@ -521,7 +521,7 @@ |
3168 | self.assertEqual(len(result['ids']), 2) |
3169 | self.assertFalse(result['messages']) |
3170 | self.assertEqual( |
3171 | - ['3', '1'], |
3172 | + [3, 1], |
3173 | values(self.read())) |
3174 | |
3175 | def test_translated(self): |
3176 | @@ -536,7 +536,7 @@ |
3177 | ], context={'lang': 'fr_FR'}) |
3178 | self.assertFalse(result['messages']) |
3179 | self.assertEqual(len(result['ids']), 2) |
3180 | - self.assertEqual(values(self.read()), ['1', '2']) |
3181 | + self.assertEqual(values(self.read()), [1, 2]) |
3182 | |
3183 | result = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) |
3184 | self.assertFalse(result['messages']) |
3185 | @@ -770,7 +770,7 @@ |
3186 | id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) |
3187 | records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) |
3188 | |
3189 | - name = lambda record: dict(record.name_get())[record.id] |
3190 | + name = lambda record: record.name_get()[0][1] |
3191 | |
3192 | result = self.import_(['value'], [ |
3193 | ['%s,%s' % (name(records[1]), name(records[2]))], |
3194 | |
3195 | === added directory 'openerp/addons/test_inherit' |
3196 | === added file 'openerp/addons/test_inherit/__init__.py' |
3197 | --- openerp/addons/test_inherit/__init__.py 1970-01-01 00:00:00 +0000 |
3198 | +++ openerp/addons/test_inherit/__init__.py 2014-05-19 13:53:27 +0000 |
3199 | @@ -0,0 +1,3 @@ |
3200 | +# -*- coding: utf-8 -*- |
3201 | +import models |
3202 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3203 | |
3204 | === added file 'openerp/addons/test_inherit/__openerp__.py' |
3205 | --- openerp/addons/test_inherit/__openerp__.py 1970-01-01 00:00:00 +0000 |
3206 | +++ openerp/addons/test_inherit/__openerp__.py 2014-05-19 13:53:27 +0000 |
3207 | @@ -0,0 +1,15 @@ |
3208 | +# -*- coding: utf-8 -*- |
3209 | +{ |
3210 | + 'name': 'test-inherit', |
3211 | + 'version': '0.1', |
3212 | + 'category': 'Tests', |
3213 | + 'description': """A module to verify the inheritance.""", |
3214 | + 'author': 'OpenERP SA', |
3215 | + 'maintainer': 'OpenERP SA', |
3216 | + 'website': 'http://www.openerp.com', |
3217 | + 'depends': ['base'], |
3218 | + 'data': [], |
3219 | + 'installable': True, |
3220 | + 'auto_install': False, |
3221 | +} |
3222 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3223 | |
3224 | === added file 'openerp/addons/test_inherit/ir.model.access.csv' |
3225 | --- openerp/addons/test_inherit/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
3226 | +++ openerp/addons/test_inherit/ir.model.access.csv 2014-05-19 13:53:27 +0000 |
3227 | @@ -0,0 +1,1 @@ |
3228 | +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
3229 | |
3230 | === added file 'openerp/addons/test_inherit/models.py' |
3231 | --- openerp/addons/test_inherit/models.py 1970-01-01 00:00:00 +0000 |
3232 | +++ openerp/addons/test_inherit/models.py 2014-05-19 13:53:27 +0000 |
3233 | @@ -0,0 +1,29 @@ |
3234 | +# -*- coding: utf-8 -*- |
3235 | +import openerp |
3236 | + |
3237 | +# We just create a new model |
3238 | +class mother(openerp.Model): |
3239 | + _name = 'test.inherit.mother' |
3240 | + |
3241 | + name = openerp.fields.Char('Name', required=True) |
3242 | + |
3243 | +# We want to inherits from the parent model and we add some fields |
3244 | +# in the child object |
3245 | +class daughter(openerp.Model): |
3246 | + _name = 'test.inherit.daugther' |
3247 | + _inherits = {'test.inherit.mother': 'template_id'} |
3248 | + |
3249 | + template_id = openerp.fields.Many2one('test.inherit.mother', 'Template', |
3250 | + required=True, ondelete='cascade') |
3251 | + field_in_daughter = openerp.fields.Char('Field1') |
3252 | + |
3253 | + |
3254 | +# We add a new field in the parent object. Because of a recent refactoring, |
3255 | +# this feature was broken. |
3256 | +# This test and these models try to show the bug and fix it. |
3257 | +class mother(openerp.Model): |
3258 | + _inherit = 'test.inherit.mother' |
3259 | + |
3260 | + field_in_mother = openerp.fields.Char() |
3261 | + |
3262 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3263 | |
3264 | === added directory 'openerp/addons/test_inherit/tests' |
3265 | === added file 'openerp/addons/test_inherit/tests/__init__.py' |
3266 | --- openerp/addons/test_inherit/tests/__init__.py 1970-01-01 00:00:00 +0000 |
3267 | +++ openerp/addons/test_inherit/tests/__init__.py 2014-05-19 13:53:27 +0000 |
3268 | @@ -0,0 +1,12 @@ |
3269 | +# -*- coding: utf-8 -*- |
3270 | + |
3271 | +from . import test_inherit |
3272 | + |
3273 | +fast_suite = [ |
3274 | +] |
3275 | + |
3276 | +checks = [ |
3277 | + test_inherit, |
3278 | +] |
3279 | + |
3280 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3281 | |
3282 | === added file 'openerp/addons/test_inherit/tests/test_inherit.py' |
3283 | --- openerp/addons/test_inherit/tests/test_inherit.py 1970-01-01 00:00:00 +0000 |
3284 | +++ openerp/addons/test_inherit/tests/test_inherit.py 2014-05-19 13:53:27 +0000 |
3285 | @@ -0,0 +1,17 @@ |
3286 | +# -*- coding: utf-8 -*- |
3287 | +from openerp.tests import common |
3288 | + |
3289 | +class test_inherits(common.TransactionCase): |
3290 | + |
3291 | + def test_access_from_child_to_parent_model(self): |
3292 | + # This test checks if the new added column of a parent model |
3293 | + # is accessible from the child model. This test has been written |
3294 | + # to verify the purpose of the inheritance computing of the class |
3295 | + # in the openerp.osv.orm._build_model. |
3296 | + mother = self.registry('test.inherit.mother') |
3297 | + daugther = self.registry('test.inherit.daugther') |
3298 | + |
3299 | + self.assertIn('field_in_mother', mother._fields) |
3300 | + self.assertIn('field_in_mother', daugther._fields) |
3301 | + |
3302 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3303 | |
3304 | === added directory 'openerp/addons/test_new_api' |
3305 | === added file 'openerp/addons/test_new_api/__init__.py' |
3306 | --- openerp/addons/test_new_api/__init__.py 1970-01-01 00:00:00 +0000 |
3307 | +++ openerp/addons/test_new_api/__init__.py 2014-05-19 13:53:27 +0000 |
3308 | @@ -0,0 +1,2 @@ |
3309 | +# -*- coding: utf-8 -*- |
3310 | +import models |
3311 | |
3312 | === added file 'openerp/addons/test_new_api/__openerp__.py' |
3313 | --- openerp/addons/test_new_api/__openerp__.py 1970-01-01 00:00:00 +0000 |
3314 | +++ openerp/addons/test_new_api/__openerp__.py 2014-05-19 13:53:27 +0000 |
3315 | @@ -0,0 +1,19 @@ |
3316 | +# -*- coding: utf-8 -*- |
3317 | +{ |
3318 | + 'name': 'Test New API', |
3319 | + 'version': '1.0', |
3320 | + 'category': 'Tests', |
3321 | + 'description': """A module to test the new API.""", |
3322 | + 'author': 'OpenERP SA', |
3323 | + 'maintainer': 'OpenERP SA', |
3324 | + 'website': 'http://www.openerp.com', |
3325 | + 'depends': ['base'], |
3326 | + 'installable': True, |
3327 | + 'auto_install': False, |
3328 | + 'data': [ |
3329 | + 'ir.model.access.csv', |
3330 | + 'views.xml', |
3331 | + 'demo_data.xml', |
3332 | + ], |
3333 | +} |
3334 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
3335 | |
3336 | === added file 'openerp/addons/test_new_api/demo_data.xml' |
3337 | --- openerp/addons/test_new_api/demo_data.xml 1970-01-01 00:00:00 +0000 |
3338 | +++ openerp/addons/test_new_api/demo_data.xml 2014-05-19 13:53:27 +0000 |
3339 | @@ -0,0 +1,30 @@ |
3340 | +<openerp> |
3341 | + <data> |
3342 | + <record id="category_0" model="test_new_api.category"> |
3343 | + <field name="name">Chat</field> |
3344 | + </record> |
3345 | + <record id="category_0_0" model="test_new_api.category"> |
3346 | + <field name="name">Foolish</field> |
3347 | + <field name="parent" ref="category_0"/> |
3348 | + </record> |
3349 | + |
3350 | + <record id="discussion_0" model="test_new_api.discussion"> |
3351 | + <field name="name">Stuff</field> |
3352 | + <field name="participants" eval="[(4, ref('base.user_root')), (4, ref('base.user_demo'))]"/> |
3353 | + </record> |
3354 | + |
3355 | + <record id="message_0_0" model="test_new_api.message"> |
3356 | + <field name="discussion" ref="discussion_0"/> |
3357 | + <field name="body">Hey dude!</field> |
3358 | + </record> |
3359 | + <record id="message_0_1" model="test_new_api.message"> |
3360 | + <field name="discussion" ref="discussion_0"/> |
3361 | + <field name="author" ref="base.user_demo"/> |
3362 | + <field name="body">What's up?</field> |
3363 | + </record> |
3364 | + <record id="message_0_2" model="test_new_api.message"> |
3365 | + <field name="discussion" ref="discussion_0"/> |
3366 | + <field name="body">This is a much longer message</field> |
3367 | + </record> |
3368 | + </data> |
3369 | +</openerp> |
3370 | |
3371 | === added file 'openerp/addons/test_new_api/ir.model.access.csv' |
3372 | --- openerp/addons/test_new_api/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
3373 | +++ openerp/addons/test_new_api/ir.model.access.csv 2014-05-19 13:53:27 +0000 |
3374 | @@ -0,0 +1,6 @@ |
3375 | +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
3376 | +access_category,test_new_api_category,test_new_api.model_test_new_api_category,,1,1,1,1 |
3377 | +access_discussion,test_new_api_discussion,test_new_api.model_test_new_api_discussion,,1,1,1,1 |
3378 | +access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1,1,1 |
3379 | +access_talk,test_new_api_talk,test_new_api.model_test_new_api_talk,,1,1,1,1 |
3380 | +access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1 |
3381 | |
3382 | === added file 'openerp/addons/test_new_api/models.py' |
3383 | --- openerp/addons/test_new_api/models.py 1970-01-01 00:00:00 +0000 |
3384 | +++ openerp/addons/test_new_api/models.py 2014-05-19 13:53:27 +0000 |
3385 | @@ -0,0 +1,183 @@ |
3386 | +# -*- coding: utf-8 -*- |
3387 | +############################################################################## |
3388 | +# |
3389 | +# OpenERP, Open Source Management Solution |
3390 | +# Copyright (C) 2013 OpenERP (<http://www.openerp.com>). |
3391 | +# |
3392 | +# This program is free software: you can redistribute it and/or modify |
3393 | +# it under the terms of the GNU Affero General Public License as |
3394 | +# published by the Free Software Foundation, either version 3 of the |
3395 | +# License, or (at your option) any later version. |
3396 | +# |
3397 | +# This program is distributed in the hope that it will be useful, |
3398 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3399 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3400 | +# GNU Affero General Public License for more details. |
3401 | +# |
3402 | +# You should have received a copy of the GNU Affero General Public License |
3403 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
3404 | +# |
3405 | +############################################################################## |
3406 | + |
3407 | +from openerp.osv import osv, fields |
3408 | + |
3409 | +class res_partner(osv.Model): |
3410 | + _inherit = 'res.partner' |
3411 | + |
3412 | + # |
3413 | + # add related fields to test them |
3414 | + # |
3415 | + _columns = { |
3416 | + # a regular one |
3417 | + 'related_company_partner_id': fields.related( |
3418 | + 'company_id', 'partner_id', type='many2one', obj='res.partner'), |
3419 | + # a related field with a single field |
3420 | + 'single_related_company_id': fields.related( |
3421 | + 'company_id', type='many2one', obj='res.company'), |
3422 | + # a related field with a single field that is also a related field! |
3423 | + 'related_related_company_id': fields.related( |
3424 | + 'single_related_company_id', type='many2one', obj='res.company'), |
3425 | + } |
3426 | + |
3427 | + |
3428 | +from openerp import Model, Integer, Float, Char, Text, Date, Selection, \ |
3429 | + Reference, Many2one, One2many, Many2many, constrains, onchange, depends, \ |
3430 | + model, one, _ |
3431 | + |
3432 | + |
3433 | +class Category(Model): |
3434 | + _name = 'test_new_api.category' |
3435 | + |
3436 | + name = Char(required=True) |
3437 | + parent = Many2one('test_new_api.category') |
3438 | + display_name = Char(store=False, readonly=True, |
3439 | + compute='_compute_display_name', inverse='_inverse_display_name') |
3440 | + |
3441 | + @one |
3442 | + @depends('name', 'parent.display_name') # this definition is recursive |
3443 | + def _compute_display_name(self): |
3444 | + if self.parent: |
3445 | + self.display_name = self.parent.display_name + ' / ' + self.name |
3446 | + else: |
3447 | + self.display_name = self.name |
3448 | + |
3449 | + @one |
3450 | + def _inverse_display_name(self): |
3451 | + names = self.display_name.split('/') |
3452 | + # determine sequence of categories |
3453 | + categories = [] |
3454 | + for name in names[:-1]: |
3455 | + category = self.search([('name', 'ilike', name.strip())]) |
3456 | + categories.append(category[0]) |
3457 | + categories.append(self) |
3458 | + # assign parents following sequence |
3459 | + for parent, child in zip(categories, categories[1:]): |
3460 | + if parent and child: |
3461 | + child.parent = parent |
3462 | + # assign name of last category, and reassign display_name (to normalize it) |
3463 | + self.name = names[-1].strip() |
3464 | + |
3465 | + |
3466 | +class Discussion(Model): |
3467 | + _name = 'test_new_api.discussion' |
3468 | + |
3469 | + name = Char(string='Title', required=True, |
3470 | + help="General description of what this discussion is about.") |
3471 | + moderator = Many2one('res.users') |
3472 | + categories = Many2many('test_new_api.category', |
3473 | + 'test_new_api_discussion_category', 'discussion', 'category') |
3474 | + participants = Many2many('res.users') |
3475 | + messages = One2many('test_new_api.message', 'discussion') |
3476 | + |
3477 | + @onchange('moderator') |
3478 | + def _onchange_moderator(self): |
3479 | + self.participants |= self.moderator |
3480 | + |
3481 | + |
3482 | +class Message(Model): |
3483 | + _name = 'test_new_api.message' |
3484 | + |
3485 | + discussion = Many2one('test_new_api.discussion', ondelete='cascade') |
3486 | + body = Text() |
3487 | + author = Many2one('res.users', default=lambda self: self.env.user) |
3488 | + name = Char(string='Title', store=True, readonly=True, |
3489 | + compute='_compute_name') |
3490 | + display_name = Char(string='Abstract', store=False, readonly=True, |
3491 | + compute='_compute_display_name') |
3492 | + size = Integer(store=False, readonly=True, |
3493 | + compute='_compute_size', search='_search_size') |
3494 | + double_size = Integer(store=False, readonly=True, |
3495 | + compute='_compute_double_size') |
3496 | + discussion_name = Char(related='discussion.name', store=False, readonly=True) |
3497 | + |
3498 | + @one |
3499 | + @constrains('author', 'discussion') |
3500 | + def _check_author(self): |
3501 | + if self.discussion and self.author not in self.discussion.participants: |
3502 | + raise ValueError(_("Author must be among the discussion participants.")) |
3503 | + |
3504 | + @one |
3505 | + @depends('author.name', 'discussion.name') |
3506 | + def _compute_name(self): |
3507 | + self.name = "[%s] %s" % (self.discussion.name or '', self.author.name) |
3508 | + |
3509 | + @one |
3510 | + @depends('author.name', 'discussion.name', 'body') |
3511 | + def _compute_display_name(self): |
3512 | + stuff = "[%s] %s: %s" % (self.author.name, self.discussion.name or '', self.body or '') |
3513 | + self.display_name = stuff[:80] |
3514 | + |
3515 | + @one |
3516 | + @depends('body') |
3517 | + def _compute_size(self): |
3518 | + self.size = len(self.body or '') |
3519 | + |
3520 | + def _search_size(self, operator, value): |
3521 | + if operator not in ('=', '!=', '<', '<=', '>', '>=', 'in', 'not in'): |
3522 | + return [] |
3523 | + # retrieve all the messages that match with a specific SQL query |
3524 | + query = """SELECT id FROM "%s" WHERE char_length("body") %s %%s""" % \ |
3525 | + (self._table, operator) |
3526 | + self.env.cr.execute(query, (value,)) |
3527 | + ids = [t[0] for t in self.env.cr.fetchall()] |
3528 | + return [('id', 'in', ids)] |
3529 | + |
3530 | + @one |
3531 | + @depends('size') |
3532 | + def _compute_double_size(self): |
3533 | + # This illustrates a subtle situation: self.double_size depends on |
3534 | + # self.size. When size is computed, self.size is assigned, which should |
3535 | + # normally invalidate self.double_size. However, this may not happen |
3536 | + # while self.double_size is being computed: the last statement below |
3537 | + # would fail, because self.double_size would be undefined. |
3538 | + self.double_size = 0 |
3539 | + size = self.size |
3540 | + self.double_size = self.double_size + size |
3541 | + |
3542 | + |
3543 | +class Talk(Model): |
3544 | + _name = 'test_new_api.talk' |
3545 | + |
3546 | + parent = Many2one('test_new_api.discussion', delegate=True) |
3547 | + |
3548 | + |
3549 | +class MixedModel(Model): |
3550 | + _name = 'test_new_api.mixed' |
3551 | + |
3552 | + number = Float(digits=(10, 2), default=3.14) |
3553 | + date = Date() |
3554 | + lang = Selection(string='Language', selection='_get_lang') |
3555 | + reference = Reference(string='Related Document', |
3556 | + selection='_reference_models') |
3557 | + |
3558 | + @model |
3559 | + def _get_lang(self): |
3560 | + langs = self.env['res.lang'].search([]) |
3561 | + return [(lang.code, lang.name) for lang in langs] |
3562 | + |
3563 | + @model |
3564 | + def _reference_models(self): |
3565 | + models = self.env['ir.model'].search([('state', '!=', 'manual')]) |
3566 | + return [(model.model, model.name) |
3567 | + for model in models |
3568 | + if not model.model.startswith('ir.')] |
3569 | |
3570 | === added directory 'openerp/addons/test_new_api/tests' |
3571 | === added file 'openerp/addons/test_new_api/tests/__init__.py' |
3572 | --- openerp/addons/test_new_api/tests/__init__.py 1970-01-01 00:00:00 +0000 |
3573 | +++ openerp/addons/test_new_api/tests/__init__.py 2014-05-19 13:53:27 +0000 |
3574 | @@ -0,0 +1,18 @@ |
3575 | +# -*- coding: utf-8 -*- |
3576 | + |
3577 | +from . import test_related |
3578 | +from . import test_new_fields |
3579 | +from . import test_onchange |
3580 | +from . import test_field_conversions |
3581 | +from . import test_attributes |
3582 | + |
3583 | +fast_suite = [ |
3584 | +] |
3585 | + |
3586 | +checks = [ |
3587 | + test_related, |
3588 | + test_new_fields, |
3589 | + test_onchange, |
3590 | + test_field_conversions, |
3591 | + test_attributes, |
3592 | +] |
3593 | |
3594 | === added file 'openerp/addons/test_new_api/tests/test_attributes.py' |
3595 | --- openerp/addons/test_new_api/tests/test_attributes.py 1970-01-01 00:00:00 +0000 |
3596 | +++ openerp/addons/test_new_api/tests/test_attributes.py 2014-05-19 13:53:27 +0000 |
3597 | @@ -0,0 +1,25 @@ |
3598 | +# -*- coding: utf-8 -*- |
3599 | +from openerp.tests import common |
3600 | + |
3601 | +ANSWER_TO_ULTIMATE_QUESTION = 42 |
3602 | + |
3603 | +class TestAttributes(common.TransactionCase): |
3604 | + |
3605 | + def test_we_can_add_attributes(self): |
3606 | + Model = self.env['test_new_api.category'] |
3607 | + instance = Model.create({'name': 'Foo'}) |
3608 | + |
3609 | + # assign an unknown attribute |
3610 | + instance.unknown = ANSWER_TO_ULTIMATE_QUESTION |
3611 | + |
3612 | + # Does the attribute exist in the instance of the model ? |
3613 | + self.assertTrue(hasattr(instance, 'unknown')) |
3614 | + |
3615 | + # Is it the right type ? |
3616 | + self.assertIsInstance(instance.unknown, (int, long)) |
3617 | + |
3618 | + # Is it the right value, in case of, we don't know ;-) |
3619 | + self.assertEqual(instance.unknown, ANSWER_TO_ULTIMATE_QUESTION) |
3620 | + |
3621 | + # We are paranoiac ! |
3622 | + self.assertEqual(getattr(instance, 'unknown'), ANSWER_TO_ULTIMATE_QUESTION) |
3623 | |
3624 | === added file 'openerp/addons/test_new_api/tests/test_field_conversions.py' |
3625 | --- openerp/addons/test_new_api/tests/test_field_conversions.py 1970-01-01 00:00:00 +0000 |
3626 | +++ openerp/addons/test_new_api/tests/test_field_conversions.py 2014-05-19 13:53:27 +0000 |
3627 | @@ -0,0 +1,11 @@ |
3628 | +# -*- coding: utf-8 -*- |
3629 | +import unittest2 |
3630 | +from openerp.osv import fields, fields2 |
3631 | + |
3632 | +class TestFieldToColumn(unittest2.TestCase): |
3633 | + def test_char(self): |
3634 | + field = fields2.Char(string="test string", required=True) |
3635 | + column = field.to_column() |
3636 | + |
3637 | + self.assertEqual(column.string, "test string") |
3638 | + self.assertTrue(column.required) |
3639 | |
3640 | === added file 'openerp/addons/test_new_api/tests/test_new_fields.py' |
3641 | --- openerp/addons/test_new_api/tests/test_new_fields.py 1970-01-01 00:00:00 +0000 |
3642 | +++ openerp/addons/test_new_api/tests/test_new_fields.py 2014-05-19 13:53:27 +0000 |
3643 | @@ -0,0 +1,346 @@ |
3644 | +# |
3645 | +# test cases for new-style fields |
3646 | +# |
3647 | +from datetime import date, datetime |
3648 | +from collections import defaultdict |
3649 | + |
3650 | +from openerp.tests import common |
3651 | + |
3652 | + |
3653 | +class TestNewFields(common.TransactionCase): |
3654 | + |
3655 | + def test_00_basics(self): |
3656 | + """ test accessing new fields """ |
3657 | + # find a discussion |
3658 | + discussion = self.env.ref('test_new_api.discussion_0') |
3659 | + |
3660 | + # read field as a record attribute or as a record item |
3661 | + self.assertIsInstance(discussion.name, basestring) |
3662 | + self.assertIsInstance(discussion['name'], basestring) |
3663 | + self.assertEqual(discussion['name'], discussion.name) |
3664 | + |
3665 | + # read it with method read() |
3666 | + values = discussion.read(['name'])[0] |
3667 | + self.assertEqual(values['name'], discussion.name) |
3668 | + |
3669 | + def test_10_non_stored(self): |
3670 | + """ test non-stored fields """ |
3671 | + # find messages |
3672 | + for message in self.env['test_new_api.message'].search([]): |
3673 | + # check definition of field |
3674 | + self.assertEqual(message.size, len(message.body or '')) |
3675 | + |
3676 | + # check recomputation after record is modified |
3677 | + size = message.size |
3678 | + message.write({'body': (message.body or '') + "!!!"}) |
3679 | + self.assertEqual(message.size, size + 3) |
3680 | + |
3681 | + def test_11_stored(self): |
3682 | + """ test stored fields """ |
3683 | + # find the demo discussion |
3684 | + discussion = self.env.ref('test_new_api.discussion_0') |
3685 | + self.assertTrue(len(discussion.messages) > 0) |
3686 | + |
3687 | + # check messages |
3688 | + name0 = discussion.name or "" |
3689 | + for message in discussion.messages: |
3690 | + self.assertEqual(message.name, "[%s] %s" % (name0, message.author.name)) |
3691 | + |
3692 | + # modify discussion name, and check again messages |
3693 | + discussion.name = name1 = 'Talking about stuff...' |
3694 | + for message in discussion.messages: |
3695 | + self.assertEqual(message.name, "[%s] %s" % (name1, message.author.name)) |
3696 | + |
3697 | + # switch message from discussion, and check again |
3698 | + name2 = 'Another discussion' |
3699 | + discussion2 = discussion.copy({'name': name2}) |
3700 | + message2 = discussion.messages[0] |
3701 | + message2.discussion = discussion2 |
3702 | + for message in discussion2.messages: |
3703 | + self.assertEqual(message.name, "[%s] %s" % (name2, message.author.name)) |
3704 | + |
3705 | + def test_12_recursive(self): |
3706 | + """ test recursively dependent fields """ |
3707 | + Category = self.env['test_new_api.category'] |
3708 | + abel = Category.create({'name': 'Abel'}) |
3709 | + beth = Category.create({'name': 'Bethany'}) |
3710 | + cath = Category.create({'name': 'Catherine'}) |
3711 | + dean = Category.create({'name': 'Dean'}) |
3712 | + ewan = Category.create({'name': 'Ewan'}) |
3713 | + finn = Category.create({'name': 'Finnley'}) |
3714 | + gabe = Category.create({'name': 'Gabriel'}) |
3715 | + |
3716 | + cath.parent = finn.parent = gabe |
3717 | + abel.parent = beth.parent = cath |
3718 | + dean.parent = ewan.parent = finn |
3719 | + |
3720 | + self.assertEqual(abel.display_name, "Gabriel / Catherine / Abel") |
3721 | + self.assertEqual(beth.display_name, "Gabriel / Catherine / Bethany") |
3722 | + self.assertEqual(cath.display_name, "Gabriel / Catherine") |
3723 | + self.assertEqual(dean.display_name, "Gabriel / Finnley / Dean") |
3724 | + self.assertEqual(ewan.display_name, "Gabriel / Finnley / Ewan") |
3725 | + self.assertEqual(finn.display_name, "Gabriel / Finnley") |
3726 | + self.assertEqual(gabe.display_name, "Gabriel") |
3727 | + |
3728 | + ewan.parent = cath |
3729 | + self.assertEqual(ewan.display_name, "Gabriel / Catherine / Ewan") |
3730 | + |
3731 | + cath.parent = finn |
3732 | + self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan") |
3733 | + |
3734 | + def test_12_cascade(self): |
3735 | + """ test computed field depending on computed field """ |
3736 | + message = self.env.ref('test_new_api.message_0_0') |
3737 | + message.invalidate_cache() |
3738 | + double_size = message.double_size |
3739 | + self.assertEqual(double_size, message.size) |
3740 | + |
3741 | + def test_13_inverse(self): |
3742 | + """ test inverse computation of fields """ |
3743 | + Category = self.env['test_new_api.category'] |
3744 | + abel = Category.create({'name': 'Abel'}) |
3745 | + beth = Category.create({'name': 'Bethany'}) |
3746 | + cath = Category.create({'name': 'Catherine'}) |
3747 | + dean = Category.create({'name': 'Dean'}) |
3748 | + ewan = Category.create({'name': 'Ewan'}) |
3749 | + finn = Category.create({'name': 'Finnley'}) |
3750 | + gabe = Category.create({'name': 'Gabriel'}) |
3751 | + self.assertEqual(ewan.display_name, "Ewan") |
3752 | + |
3753 | + ewan.display_name = "Abel / Bethany / Catherine / Erwan" |
3754 | + |
3755 | + self.assertEqual(beth.parent, abel) |
3756 | + self.assertEqual(cath.parent, beth) |
3757 | + self.assertEqual(ewan.parent, cath) |
3758 | + self.assertEqual(ewan.name, "Erwan") |
3759 | + |
3760 | + def test_14_search(self): |
3761 | + """ test search on computed fields """ |
3762 | + discussion = self.env.ref('test_new_api.discussion_0') |
3763 | + |
3764 | + # determine message sizes |
3765 | + sizes = set(message.size for message in discussion.messages) |
3766 | + |
3767 | + # search for messages based on their size |
3768 | + for size in sizes: |
3769 | + messages0 = self.env['test_new_api.message'].search( |
3770 | + [('discussion', '=', discussion.id), ('size', '<=', size)]) |
3771 | + |
3772 | + messages1 = self.env['test_new_api.message'].browse() |
3773 | + for message in discussion.messages: |
3774 | + if message.size <= size: |
3775 | + messages1 += message |
3776 | + |
3777 | + self.assertEqual(messages0, messages1) |
3778 | + |
3779 | + def test_15_constraint(self): |
3780 | + """ test new-style Python constraints """ |
3781 | + discussion = self.env.ref('test_new_api.discussion_0') |
3782 | + |
3783 | + # remove oneself from discussion participants: we can no longer create |
3784 | + # messages in discussion |
3785 | + discussion.participants -= self.env.user |
3786 | + with self.assertRaises(Exception): |
3787 | + self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'}) |
3788 | + |
3789 | + # put back oneself into discussion participants: now we can create |
3790 | + # messages in discussion |
3791 | + discussion.participants += self.env.user |
3792 | + self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'}) |
3793 | + |
3794 | + def test_20_float(self): |
3795 | + """ test float fields """ |
3796 | + record = self.env['test_new_api.mixed'].create({}) |
3797 | + |
3798 | + # assign value, and expect rounding |
3799 | + record.write({'number': 2.4999999999999996}) |
3800 | + self.assertEqual(record.number, 2.50) |
3801 | + |
3802 | + # same with field setter |
3803 | + record.number = 2.4999999999999996 |
3804 | + self.assertEqual(record.number, 2.50) |
3805 | + |
3806 | + def test_21_date(self): |
3807 | + """ test date fields """ |
3808 | + record = self.env['test_new_api.mixed'].create({}) |
3809 | + |
3810 | + # one may assign False or None |
3811 | + record.date = None |
3812 | + self.assertFalse(record.date) |
3813 | + |
3814 | + # one may assign date and datetime objects |
3815 | + record.date = date(2012, 05, 01) |
3816 | + self.assertEqual(record.date, '2012-05-01') |
3817 | + |
3818 | + record.date = datetime(2012, 05, 01, 10, 45, 00) |
3819 | + self.assertEqual(record.date, '2012-05-01') |
3820 | + |
3821 | + # one may assign dates in the default format, and it must be checked |
3822 | + record.date = '2012-05-01' |
3823 | + self.assertEqual(record.date, '2012-05-01') |
3824 | + |
3825 | + with self.assertRaises(ValueError): |
3826 | + record.date = '12-5-1' |
3827 | + |
3828 | + def test_22_selection(self): |
3829 | + """ test selection fields """ |
3830 | + record = self.env['test_new_api.mixed'].create({}) |
3831 | + |
3832 | + # one may assign False or None |
3833 | + record.lang = None |
3834 | + self.assertFalse(record.lang) |
3835 | + |
3836 | + # one may assign a value, and it must be checked |
3837 | + for language in self.env['res.lang'].search([]): |
3838 | + record.lang = language.code |
3839 | + with self.assertRaises(ValueError): |
3840 | + record.lang = 'zz_ZZ' |
3841 | + |
3842 | + def test_23_relation(self): |
3843 | + """ test relation fields """ |
3844 | + demo = self.env.ref('base.user_demo') |
3845 | + message = self.env.ref('test_new_api.message_0_0') |
3846 | + |
3847 | + # check environment of record and related records |
3848 | + self.assertEqual(message.env, self.env) |
3849 | + self.assertEqual(message.discussion.env, self.env) |
3850 | + |
3851 | + demo_env = self.env(user=demo) |
3852 | + self.assertNotEqual(demo_env, self.env) |
3853 | + |
3854 | + # check environment of record and related records |
3855 | + self.assertEqual(message.env, self.env) |
3856 | + self.assertEqual(message.discussion.env, self.env) |
3857 | + |
3858 | + # "migrate" message into demo_env, and check again |
3859 | + demo_message = message.sudo(user=demo) |
3860 | + self.assertEqual(demo_message.env, demo_env) |
3861 | + self.assertEqual(demo_message.discussion.env, demo_env) |
3862 | + |
3863 | + # assign record's parent to a record in demo_env |
3864 | + message.discussion = message.discussion.copy({'name': 'Copy'}) |
3865 | + |
3866 | + # both message and its parent field must be in self.env |
3867 | + self.assertEqual(message.env, self.env) |
3868 | + self.assertEqual(message.discussion.env, self.env) |
3869 | + |
3870 | + def test_24_reference(self): |
3871 | + """ test reference fields. """ |
3872 | + record = self.env['test_new_api.mixed'].create({}) |
3873 | + |
3874 | + # one may assign False or None |
3875 | + record.reference = None |
3876 | + self.assertFalse(record.reference) |
3877 | + |
3878 | + # one may assign a user or a partner... |
3879 | + record.reference = self.env.user |
3880 | + self.assertEqual(record.reference, self.env.user) |
3881 | + record.reference = self.env.user.partner_id |
3882 | + self.assertEqual(record.reference, self.env.user.partner_id) |
3883 | + # ... but no record from a model that starts with 'ir.' |
3884 | + with self.assertRaises(ValueError): |
3885 | + record.reference = self.env['ir.model'].search([], limit=1) |
3886 | + |
3887 | + def test_25_related(self): |
3888 | + """ test related fields. """ |
3889 | + message = self.env.ref('test_new_api.message_0_0') |
3890 | + discussion = message.discussion |
3891 | + |
3892 | + # check value of related field |
3893 | + self.assertEqual(message.discussion_name, discussion.name) |
3894 | + |
3895 | + # change discussion name, and check result |
3896 | + discussion.name = 'Foo' |
3897 | + self.assertEqual(message.discussion_name, 'Foo') |
3898 | + |
3899 | + # change discussion name via related field, and check result |
3900 | + message.discussion_name = 'Bar' |
3901 | + self.assertEqual(discussion.name, 'Bar') |
3902 | + self.assertEqual(message.discussion_name, 'Bar') |
3903 | + |
3904 | + # search on related field, and check result |
3905 | + search_on_related = self.env['test_new_api.message'].search([('discussion_name', '=', 'Bar')]) |
3906 | + search_on_regular = self.env['test_new_api.message'].search([('discussion.name', '=', 'Bar')]) |
3907 | + self.assertEqual(search_on_related, search_on_regular) |
3908 | + |
3909 | + # check that field attributes are copied |
3910 | + message_field = message.fields_get(['discussion_name'])['discussion_name'] |
3911 | + discussion_field = discussion.fields_get(['name'])['name'] |
3912 | + self.assertEqual(message_field['help'], discussion_field['help']) |
3913 | + |
3914 | + def test_26_inherited(self): |
3915 | + """ test inherited fields. """ |
3916 | + # a bunch of fields are inherited from res_partner |
3917 | + for user in self.env['res.users'].search([]): |
3918 | + partner = user.partner_id |
3919 | + for field in ('is_company', 'name', 'email', 'country_id'): |
3920 | + self.assertEqual(getattr(user, field), getattr(partner, field)) |
3921 | + self.assertEqual(user[field], partner[field]) |
3922 | + |
3923 | + def test_30_read(self): |
3924 | + """ test computed fields as returned by read(). """ |
3925 | + discussion = self.env.ref('test_new_api.discussion_0') |
3926 | + |
3927 | + for message in discussion.messages: |
3928 | + display_name = message.display_name |
3929 | + size = message.size |
3930 | + |
3931 | + data = message.read(['display_name', 'size'])[0] |
3932 | + self.assertEqual(data['display_name'], display_name) |
3933 | + self.assertEqual(data['size'], size) |
3934 | + |
3935 | + def test_40_new(self): |
3936 | + """ test new records. """ |
3937 | + discussion = self.env.ref('test_new_api.discussion_0') |
3938 | + |
3939 | + # create a new message |
3940 | + message = self.env['test_new_api.message'].new() |
3941 | + self.assertFalse(message.id) |
3942 | + |
3943 | + # assign some fields; should have no side effect |
3944 | + message.discussion = discussion |
3945 | + message.body = BODY = "May the Force be with you." |
3946 | + self.assertEqual(message.discussion, discussion) |
3947 | + self.assertEqual(message.body, BODY) |
3948 | + |
3949 | + self.assertNotIn(message, discussion.messages) |
3950 | + |
3951 | + # check computed values of fields |
3952 | + user = self.env.user |
3953 | + self.assertEqual(message.author, user) |
3954 | + self.assertEqual(message.name, "[%s] %s" % (discussion.name, user.name)) |
3955 | + self.assertEqual(message.size, len(BODY)) |
3956 | + |
3957 | + def test_41_defaults(self): |
3958 | + """ test default values. """ |
3959 | + fields = ['discussion', 'body', 'author', 'size'] |
3960 | + defaults = self.env['test_new_api.message'].default_get(fields) |
3961 | + self.assertEqual(defaults, {'author': self.env.uid, 'size': 0}) |
3962 | + |
3963 | + defaults = self.env['test_new_api.mixed'].default_get(['number']) |
3964 | + self.assertEqual(defaults, {'number': 3.14}) |
3965 | + |
3966 | + |
3967 | +class TestMagicFields(common.TransactionCase): |
3968 | + |
3969 | + def test_write_date(self): |
3970 | + record = self.env['test_new_api.discussion'].create({'name': 'Booba'}) |
3971 | + self.assertEqual(record.create_uid, self.env.user) |
3972 | + self.assertEqual(record.write_uid, self.env.user) |
3973 | + |
3974 | + |
3975 | +class TestInherits(common.TransactionCase): |
3976 | + |
3977 | + def test_inherits(self): |
3978 | + """ Check that a many2one field with delegate=True adds an entry in _inherits """ |
3979 | + Talk = self.env['test_new_api.talk'] |
3980 | + self.assertEqual(Talk._inherits, {'test_new_api.discussion': 'parent'}) |
3981 | + self.assertIn('name', Talk._fields) |
3982 | + self.assertEqual(Talk._fields['name'].related, ('parent', 'name')) |
3983 | + |
3984 | + talk = Talk.create({'name': 'Foo'}) |
3985 | + discussion = talk.parent |
3986 | + self.assertTrue(discussion) |
3987 | + self.assertEqual(talk._name, 'test_new_api.talk') |
3988 | + self.assertEqual(discussion._name, 'test_new_api.discussion') |
3989 | + self.assertEqual(talk.name, discussion.name) |
3990 | |
3991 | === added file 'openerp/addons/test_new_api/tests/test_onchange.py' |
3992 | --- openerp/addons/test_new_api/tests/test_onchange.py 1970-01-01 00:00:00 +0000 |
3993 | +++ openerp/addons/test_new_api/tests/test_onchange.py 2014-05-19 13:53:27 +0000 |
3994 | @@ -0,0 +1,159 @@ |
3995 | +# -*- coding: utf-8 -*- |
3996 | + |
3997 | +from openerp.tests import common |
3998 | + |
3999 | +class TestOnChange(common.TransactionCase): |
4000 | + |
4001 | + def setUp(self): |
4002 | + super(TestOnChange, self).setUp() |
4003 | + self.Discussion = self.env['test_new_api.discussion'] |
4004 | + self.Message = self.env['test_new_api.message'] |
4005 | + |
4006 | + def test_default_get(self): |
4007 | + """ checking values returned by default_get() """ |
4008 | + fields = ['name', 'categories', 'participants', 'messages'] |
4009 | + values = self.Discussion.default_get(fields) |
4010 | + self.assertEqual(values, {}) |
4011 | + |
4012 | + def test_get_field(self): |
4013 | + """ checking that accessing an unknown attribute does nothing special """ |
4014 | + with self.assertRaises(AttributeError): |
4015 | + self.Discussion.not_really_a_method() |
4016 | + |
4017 | + def test_new_onchange(self): |
4018 | + """ test the effect of onchange() """ |
4019 | + discussion = self.env.ref('test_new_api.discussion_0') |
4020 | + BODY = "What a beautiful day!" |
4021 | + USER = self.env.user |
4022 | + |
4023 | + self.env.invalidate_all() |
4024 | + result = self.Message.onchange({ |
4025 | + 'discussion': discussion.id, |
4026 | + 'name': "[%s] %s" % ('', USER.name), |
4027 | + 'body': False, |
4028 | + 'author': USER.id, |
4029 | + 'size': 0, |
4030 | + }, 'discussion') |
4031 | + self.assertEqual(result['value'], { |
4032 | + 'name': "[%s] %s" % (discussion.name, USER.name), |
4033 | + }) |
4034 | + |
4035 | + self.env.invalidate_all() |
4036 | + result = self.Message.onchange({ |
4037 | + 'discussion': discussion.id, |
4038 | + 'name': "[%s] %s" % (discussion.name, USER.name), |
4039 | + 'body': BODY, |
4040 | + 'author': USER.id, |
4041 | + 'size': 0, |
4042 | + }, 'body') |
4043 | + self.assertEqual(result['value'], { |
4044 | + 'size': len(BODY), |
4045 | + }) |
4046 | + |
4047 | + def test_new_onchange_one2many(self): |
4048 | + """ test the effect of onchange() on one2many fields """ |
4049 | + tocheck = ['messages.name', 'messages.body', 'messages.author', 'messages.size'] |
4050 | + BODY = "What a beautiful day!" |
4051 | + USER = self.env.user |
4052 | + |
4053 | + # create an independent message |
4054 | + message = self.Message.create({'body': BODY}) |
4055 | + self.assertEqual(message.name, "[%s] %s" % ('', USER.name)) |
4056 | + |
4057 | + # modify messages |
4058 | + self.env.invalidate_all() |
4059 | + result = self.Discussion.onchange({ |
4060 | + 'name': "Foo", |
4061 | + 'categories': [], |
4062 | + 'moderator': False, |
4063 | + 'participants': [], |
4064 | + 'messages': [ |
4065 | + (0, 0, { |
4066 | + 'name': "[%s] %s" % ('', USER.name), |
4067 | + 'body': BODY, |
4068 | + 'author': USER.id, |
4069 | + 'size': len(BODY), |
4070 | + }), |
4071 | + (1, message.id, { |
4072 | + 'name': "[%s] %s" % ('', USER.name), |
4073 | + 'body': BODY, |
4074 | + 'author': USER.id, |
4075 | + 'size': len(BODY), |
4076 | + }), |
4077 | + ], |
4078 | + }, 'messages', tocheck) |
4079 | + self.assertItemsEqual(list(result['value']), ['messages']) |
4080 | + self.assertItemsEqual(result['value']['messages'], [ |
4081 | + (0, 0, { |
4082 | + 'name': "[%s] %s" % ("Foo", USER.name), |
4083 | + 'body': BODY, |
4084 | + 'author': USER.id, |
4085 | + 'size': len(BODY), |
4086 | + }), |
4087 | + (1, message.id, { |
4088 | + 'name': "[%s] %s" % ("Foo", USER.name), |
4089 | + 'body': BODY, |
4090 | + 'author': USER.id, |
4091 | + 'size': len(BODY), |
4092 | + }), |
4093 | + ]) |
4094 | + |
4095 | + # modify discussion name |
4096 | + self.env.invalidate_all() |
4097 | + result = self.Discussion.onchange({ |
4098 | + 'name': "Foo", |
4099 | + 'categories': [], |
4100 | + 'moderator': False, |
4101 | + 'participants': [], |
4102 | + 'messages': [ |
4103 | + (0, 0, { |
4104 | + 'name': "[%s] %s" % ('', USER.name), |
4105 | + 'body': BODY, |
4106 | + 'author': USER.id, |
4107 | + 'size': len(BODY), |
4108 | + }), |
4109 | + (4, message.id), |
4110 | + ], |
4111 | + }, 'name', tocheck) |
4112 | + self.assertItemsEqual(list(result['value']), ['messages']) |
4113 | + self.assertItemsEqual(result['value']['messages'], [ |
4114 | + (0, 0, { |
4115 | + 'name': "[%s] %s" % ("Foo", USER.name), |
4116 | + 'body': BODY, |
4117 | + 'author': USER.id, |
4118 | + 'size': len(BODY), |
4119 | + }), |
4120 | + (1, message.id, { |
4121 | + 'name': "[%s] %s" % ("Foo", USER.name), |
4122 | + 'body': BODY, |
4123 | + 'author': USER.id, |
4124 | + 'size': len(BODY), |
4125 | + }), |
4126 | + ]) |
4127 | + |
4128 | + def test_new_onchange_specific(self): |
4129 | + """ test the effect of field-specific onchange method """ |
4130 | + discussion = self.env.ref('test_new_api.discussion_0') |
4131 | + demo = self.env.ref('base.user_demo') |
4132 | + |
4133 | + # first remove demo user from participants |
4134 | + discussion.participants -= demo |
4135 | + self.assertNotIn(demo, discussion.participants) |
4136 | + |
4137 | + # check that demo_user is added to participants when set as moderator |
4138 | + name = discussion.name |
4139 | + categories = [(4, cat.id) for cat in discussion.categories] |
4140 | + participants = [(4, usr.id) for usr in discussion.participants] |
4141 | + messages = [(4, msg.id) for msg in discussion.messages] |
4142 | + |
4143 | + self.env.invalidate_all() |
4144 | + result = discussion.onchange({ |
4145 | + 'name': name, |
4146 | + 'categories': categories, |
4147 | + 'moderator': demo.id, |
4148 | + 'participants': participants, |
4149 | + 'messages': messages, |
4150 | + }, 'moderator') |
4151 | + |
4152 | + self.assertItemsEqual(list(result['value']), ['participants']) |
4153 | + self.assertItemsEqual(result['value']['participants'], participants + [(4, demo.id)]) |
4154 | |
4155 | === renamed file 'openerp/addons/base/tests/test_fields.py' => 'openerp/addons/test_new_api/tests/test_related.py' |
4156 | --- openerp/addons/base/tests/test_fields.py 2014-03-11 13:38:50 +0000 |
4157 | +++ openerp/addons/test_new_api/tests/test_related.py 2014-05-19 13:53:27 +0000 |
4158 | @@ -1,6 +1,8 @@ |
4159 | # |
4160 | -# test cases for fields access, etc. |
4161 | +# test cases for related fields, etc. |
4162 | # |
4163 | +import unittest |
4164 | + |
4165 | from openerp.osv import fields |
4166 | from openerp.tests import common |
4167 | |
4168 | @@ -13,13 +15,6 @@ |
4169 | |
4170 | def test_0_related(self): |
4171 | """ test an usual related field """ |
4172 | - # add a related field test_related_company_id on res.partner |
4173 | - old_columns = self.partner._columns |
4174 | - self.partner._columns = dict(old_columns) |
4175 | - self.partner._columns.update({ |
4176 | - 'related_company_partner_id': fields.related('company_id', 'partner_id', type='many2one', obj='res.partner'), |
4177 | - }) |
4178 | - |
4179 | # find a company with a non-null partner_id |
4180 | ids = self.company.search(self.cr, self.uid, [('partner_id', '!=', False)], limit=1) |
4181 | id = ids[0] |
4182 | @@ -30,9 +25,6 @@ |
4183 | partner_ids2 = self.partner.search(self.cr, self.uid, [('related_company_partner_id', '=', id)]) |
4184 | self.assertEqual(partner_ids1, partner_ids2) |
4185 | |
4186 | - # restore res.partner fields |
4187 | - self.partner._columns = old_columns |
4188 | - |
4189 | def do_test_company_field(self, field): |
4190 | # get a partner with a non-null company_id |
4191 | ids = self.partner.search(self.cr, self.uid, [('company_id', '!=', False)], limit=1) |
4192 | @@ -48,57 +40,14 @@ |
4193 | |
4194 | def test_1_single_related(self): |
4195 | """ test a related field with a single indirection like fields.related('foo') """ |
4196 | - # add a related field test_related_company_id on res.partner |
4197 | - # and simulate a _inherits_reload() to populate _all_columns. |
4198 | - old_columns = self.partner._columns |
4199 | - old_all_columns = self.partner._all_columns |
4200 | - self.partner._columns = dict(old_columns) |
4201 | - self.partner._all_columns = dict(old_all_columns) |
4202 | - self.partner._columns.update({ |
4203 | - 'single_related_company_id': fields.related('company_id', type='many2one', obj='res.company'), |
4204 | - }) |
4205 | - self.partner._all_columns.update({ |
4206 | - 'single_related_company_id': fields.column_info('single_related_company_id', self.partner._columns['single_related_company_id'], None, None, None) |
4207 | - }) |
4208 | - |
4209 | self.do_test_company_field('single_related_company_id') |
4210 | |
4211 | - # restore res.partner fields |
4212 | - self.partner._columns = old_columns |
4213 | - self.partner._all_columns = old_all_columns |
4214 | - |
4215 | def test_2_related_related(self): |
4216 | """ test a related field referring to a related field """ |
4217 | - # add a related field on a related field on res.partner |
4218 | - # and simulate a _inherits_reload() to populate _all_columns. |
4219 | - old_columns = self.partner._columns |
4220 | - old_all_columns = self.partner._all_columns |
4221 | - self.partner._columns = dict(old_columns) |
4222 | - self.partner._all_columns = dict(old_all_columns) |
4223 | - self.partner._columns.update({ |
4224 | - 'single_related_company_id': fields.related('company_id', type='many2one', obj='res.company'), |
4225 | - 'related_related_company_id': fields.related('single_related_company_id', type='many2one', obj='res.company'), |
4226 | - }) |
4227 | - self.partner._all_columns.update({ |
4228 | - 'single_related_company_id': fields.column_info('single_related_company_id', self.partner._columns['single_related_company_id'], None, None, None), |
4229 | - 'related_related_company_id': fields.column_info('related_related_company_id', self.partner._columns['related_related_company_id'], None, None, None) |
4230 | - }) |
4231 | - |
4232 | self.do_test_company_field('related_related_company_id') |
4233 | |
4234 | - # restore res.partner fields |
4235 | - self.partner._columns = old_columns |
4236 | - self.partner._all_columns = old_all_columns |
4237 | - |
4238 | def test_3_read_write(self): |
4239 | """ write on a related field """ |
4240 | - # add a related field test_related_company_id on res.partner |
4241 | - old_columns = self.partner._columns |
4242 | - self.partner._columns = dict(old_columns) |
4243 | - self.partner._columns.update({ |
4244 | - 'related_company_partner_id': fields.related('company_id', 'partner_id', type='many2one', obj='res.partner'), |
4245 | - }) |
4246 | - |
4247 | # find a company with a non-null partner_id |
4248 | company_ids = self.company.search(self.cr, self.uid, [('partner_id', '!=', False)], limit=1) |
4249 | company = self.company.browse(self.cr, self.uid, company_ids[0]) |
4250 | @@ -117,9 +66,6 @@ |
4251 | partner = self.partner.browse(self.cr, self.uid, partner_ids[0]) |
4252 | self.assertEqual(partner.related_company_partner_id.id, new_partner_id) |
4253 | |
4254 | - # restore res.partner fields |
4255 | - self.partner._columns = old_columns |
4256 | - |
4257 | |
4258 | class TestPropertyField(common.TransactionCase): |
4259 | |
4260 | @@ -132,6 +78,7 @@ |
4261 | self.property = self.registry('ir.property') |
4262 | self.imd = self.registry('ir.model.data') |
4263 | |
4264 | + @unittest.skip("invalid monkey-patching") |
4265 | def test_1_property_multicompany(self): |
4266 | cr, uid = self.cr, self.uid |
4267 | |
4268 | |
4269 | === added file 'openerp/addons/test_new_api/views.xml' |
4270 | --- openerp/addons/test_new_api/views.xml 1970-01-01 00:00:00 +0000 |
4271 | +++ openerp/addons/test_new_api/views.xml 2014-05-19 13:53:27 +0000 |
4272 | @@ -0,0 +1,127 @@ |
4273 | +<openerp> |
4274 | + <data> |
4275 | + <menuitem id="menu_main" name="Discussions" sequence="20"/> |
4276 | + |
4277 | + <menuitem id="menu_sub" name="Discussions" parent="menu_main" sequence="10"/> |
4278 | + |
4279 | + <record id="action_discussions" model="ir.actions.act_window"> |
4280 | + <field name="name">Discussions</field> |
4281 | + <field name="res_model">test_new_api.discussion</field> |
4282 | + <field name="view_mode">tree,form</field> |
4283 | + </record> |
4284 | + <menuitem id="menu_discussions" action="action_discussions" parent="menu_sub" sequence="10"/> |
4285 | + |
4286 | + <record id="action_messages" model="ir.actions.act_window"> |
4287 | + <field name="name">Messages</field> |
4288 | + <field name="res_model">test_new_api.message</field> |
4289 | + <field name="view_mode">tree,form</field> |
4290 | + </record> |
4291 | + <menuitem id="menu_messages" action="action_messages" parent="menu_sub" sequence="20"/> |
4292 | + |
4293 | + <menuitem id="menu_config" name="Configuration" parent="menu_main" sequence="20"/> |
4294 | + |
4295 | + <record id="action_categories" model="ir.actions.act_window"> |
4296 | + <field name="name">Categories</field> |
4297 | + <field name="res_model">test_new_api.category</field> |
4298 | + <field name="view_mode">tree,form</field> |
4299 | + </record> |
4300 | + <menuitem id="menu_categories" action="action_categories" parent="menu_config"/> |
4301 | + |
4302 | + <!-- Discussion form view --> |
4303 | + <record id="discussion_form" model="ir.ui.view"> |
4304 | + <field name="name">discussion form view</field> |
4305 | + <field name="model">test_new_api.discussion</field> |
4306 | + <field name="arch" type="xml"> |
4307 | + <form string="Discussion" version="7.0"> |
4308 | + <sheet> |
4309 | + <group> |
4310 | + <field name="name"/> |
4311 | + <field name="categories" widget="many2many_tags"/> |
4312 | + <field name="moderator"/> |
4313 | + </group> |
4314 | + <notebook> |
4315 | + <page string="Messages"> |
4316 | + <field name="messages"> |
4317 | + <tree name="Messages"> |
4318 | + <field name="name"/> |
4319 | + <field name="body"/> |
4320 | + </tree> |
4321 | + <form string="Message" version="7.0"> |
4322 | + <group> |
4323 | + <field name="name"/> |
4324 | + <field name="author"/> |
4325 | + <field name="size"/> |
4326 | + </group> |
4327 | + <label for="body"/> |
4328 | + <field name="body"/> |
4329 | + </form> |
4330 | + </field> |
4331 | + </page> |
4332 | + <page string="Participants"> |
4333 | + <field name="participants"/> |
4334 | + </page> |
4335 | + </notebook> |
4336 | + </sheet> |
4337 | + </form> |
4338 | + </field> |
4339 | + </record> |
4340 | + |
4341 | + <!-- Message tree view --> |
4342 | + <record id="message_tree" model="ir.ui.view"> |
4343 | + <field name="name">message tree view</field> |
4344 | + <field name="model">test_new_api.message</field> |
4345 | + <field name="arch" type="xml"> |
4346 | + <tree string="Messages"> |
4347 | + <field name="display_name"/> |
4348 | + </tree> |
4349 | + </field> |
4350 | + </record> |
4351 | + |
4352 | + <!-- Message form view --> |
4353 | + <record id="message_form" model="ir.ui.view"> |
4354 | + <field name="name">message form view</field> |
4355 | + <field name="model">test_new_api.message</field> |
4356 | + <field name="arch" type="xml"> |
4357 | + <form string="Message" version="7.0"> |
4358 | + <sheet> |
4359 | + <group> |
4360 | + <field name="discussion"/> |
4361 | + <field name="name"/> |
4362 | + <field name="author"/> |
4363 | + <field name="size"/> |
4364 | + </group> |
4365 | + <label for="body"/> |
4366 | + <field name="body"/> |
4367 | + </sheet> |
4368 | + </form> |
4369 | + </field> |
4370 | + </record> |
4371 | + |
4372 | + <!-- Category tree view --> |
4373 | + <record id="category_tree" model="ir.ui.view"> |
4374 | + <field name="name">category tree view</field> |
4375 | + <field name="model">test_new_api.category</field> |
4376 | + <field name="arch" type="xml"> |
4377 | + <tree string="Categories"> |
4378 | + <field name="display_name"/> |
4379 | + </tree> |
4380 | + </field> |
4381 | + </record> |
4382 | + |
4383 | + <!-- Category form view --> |
4384 | + <record id="category_form" model="ir.ui.view"> |
4385 | + <field name="name">category form view</field> |
4386 | + <field name="model">test_new_api.category</field> |
4387 | + <field name="arch" type="xml"> |
4388 | + <form string="Category" version="7.0"> |
4389 | + <sheet> |
4390 | + <group> |
4391 | + <field name="name"/> |
4392 | + <field name="parent"/> |
4393 | + </group> |
4394 | + </sheet> |
4395 | + </form> |
4396 | + </field> |
4397 | + </record> |
4398 | + </data> |
4399 | +</openerp> |
4400 | |
4401 | === modified file 'openerp/addons/test_workflow/models.py' |
4402 | --- openerp/addons/test_workflow/models.py 2013-07-26 10:36:00 +0000 |
4403 | +++ openerp/addons/test_workflow/models.py 2014-05-19 13:53:27 +0000 |
4404 | @@ -56,12 +56,13 @@ |
4405 | _inherit = 'test.workflow.model.a' |
4406 | |
4407 | for name in 'bcdefghijkl': |
4408 | - type( |
4409 | - name, |
4410 | - (openerp.osv.orm.Model,), |
4411 | - { |
4412 | - '_name': 'test.workflow.model.%s' % name, |
4413 | - '_inherit': 'test.workflow.model.a', |
4414 | - }) |
4415 | + # |
4416 | + # Do not use type() to create the class here, but use the class construct. |
4417 | + # This is because the __module__ of the new class would be the one of the |
4418 | + # metaclass that provides method __new__! |
4419 | + # |
4420 | + class NewModel(openerp.osv.orm.Model): |
4421 | + _name = 'test.workflow.model.%s' % name |
4422 | + _inherit = 'test.workflow.model.a' |
4423 | |
4424 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
4425 | |
4426 | === modified file 'openerp/addons/test_workflow/tests/test_workflow.py' |
4427 | --- openerp/addons/test_workflow/tests/test_workflow.py 2013-07-26 10:36:00 +0000 |
4428 | +++ openerp/addons/test_workflow/tests/test_workflow.py 2014-05-19 13:53:27 +0000 |
4429 | @@ -53,7 +53,7 @@ |
4430 | |
4431 | # b -> c is a trigger (which is False), |
4432 | # so we remain in the b activity. |
4433 | - model.trigger(self.cr, SUPERUSER_ID, [i]) |
4434 | + model.trigger(self.cr, SUPERUSER_ID) |
4435 | self.check_activities(model._name, i, ['b']) |
4436 | |
4437 | # b -> c is a trigger (which is set to True). |
4438 | |
4439 | === modified file 'openerp/exceptions.py' |
4440 | --- openerp/exceptions.py 2013-02-15 14:35:03 +0000 |
4441 | +++ openerp/exceptions.py 2014-05-19 13:53:27 +0000 |
4442 | @@ -28,6 +28,13 @@ |
4443 | If you consider introducing new exceptions, check out the test_exceptions addon. |
4444 | """ |
4445 | |
4446 | +# kept for backward compatibility |
4447 | +class except_orm(Exception): |
4448 | + def __init__(self, name, value): |
4449 | + self.name = name |
4450 | + self.value = value |
4451 | + self.args = (name, value) |
4452 | + |
4453 | class Warning(Exception): |
4454 | pass |
4455 | |
4456 | @@ -47,8 +54,15 @@ |
4457 | super(AccessDenied, self).__init__('Access denied.') |
4458 | self.traceback = ('', '', '') |
4459 | |
4460 | -class AccessError(Exception): |
4461 | +class AccessError(except_orm): |
4462 | """ Access rights error. """ |
4463 | + def __init__(self, msg): |
4464 | + super(AccessError, self).__init__('AccessError', msg) |
4465 | + |
4466 | +class MissingError(except_orm): |
4467 | + """ Missing record(s). """ |
4468 | + def __init__(self, msg): |
4469 | + super(MissingError, self).__init__('MissingError', msg) |
4470 | |
4471 | class DeferredException(Exception): |
4472 | """ Exception object holding a traceback for asynchronous reporting. |
4473 | |
4474 | === modified file 'openerp/http.py' |
4475 | --- openerp/http.py 2014-05-08 08:39:21 +0000 |
4476 | +++ openerp/http.py 2014-05-19 13:53:27 +0000 |
4477 | @@ -35,6 +35,7 @@ |
4478 | import werkzeug.wsgi |
4479 | |
4480 | import openerp |
4481 | +from openerp import SUPERUSER_ID |
4482 | from openerp.service import security, model as service_model |
4483 | from openerp.tools.func import lazy_property |
4484 | |
4485 | |
4486 | === modified file 'openerp/modules/loading.py' |
4487 | --- openerp/modules/loading.py 2014-04-18 14:15:50 +0000 |
4488 | +++ openerp/modules/loading.py 2014-05-19 13:53:27 +0000 |
4489 | @@ -178,6 +178,7 @@ |
4490 | status['progress'] = (index + 0.75) / len(graph) |
4491 | _load_data(cr, module_name, idref, mode, kind='demo') |
4492 | cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id)) |
4493 | + modobj.invalidate_cache(cr, SUPERUSER_ID, ['demo'], [module_id]) |
4494 | |
4495 | migrations.migrate_module(package, 'post') |
4496 | |
4497 | @@ -315,6 +316,7 @@ |
4498 | modobj.button_upgrade(cr, SUPERUSER_ID, ids) |
4499 | |
4500 | cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) |
4501 | + modobj.invalidate_cache(cr, SUPERUSER_ID, ['state']) |
4502 | |
4503 | |
4504 | # STEP 3: Load marked modules (skipping base which was done in STEP 1) |
4505 | |
4506 | === modified file 'openerp/modules/module.py' |
4507 | --- openerp/modules/module.py 2014-05-05 12:18:40 +0000 |
4508 | +++ openerp/modules/module.py 2014-05-19 13:53:27 +0000 |
4509 | @@ -243,7 +243,7 @@ |
4510 | for obj in obj_list: |
4511 | obj._auto_end(cr, {'module': module_name}) |
4512 | cr.commit() |
4513 | - todo.sort() |
4514 | + todo.sort(key=lambda x: x[0]) |
4515 | for t in todo: |
4516 | t[1](cr, *t[2]) |
4517 | cr.commit() |
4518 | |
4519 | === modified file 'openerp/modules/registry.py' |
4520 | --- openerp/modules/registry.py 2014-04-14 07:59:06 +0000 |
4521 | +++ openerp/modules/registry.py 2014-05-19 13:53:27 +0000 |
4522 | @@ -27,12 +27,9 @@ |
4523 | import logging |
4524 | import threading |
4525 | |
4526 | -import openerp.sql_db |
4527 | -import openerp.osv.orm |
4528 | -import openerp.tools |
4529 | -import openerp.modules.db |
4530 | -import openerp.tools.config |
4531 | -from openerp.tools import assertion_report |
4532 | +import openerp |
4533 | +from openerp import SUPERUSER_ID |
4534 | +from openerp.tools import assertion_report, lazy_property |
4535 | |
4536 | _logger = logging.getLogger(__name__) |
4537 | |
4538 | @@ -49,6 +46,7 @@ |
4539 | self.models = {} # model name/model instance mapping |
4540 | self._sql_error = {} |
4541 | self._store_function = {} |
4542 | + self._pure_function_fields = {} # {model: [field, ...], ...} |
4543 | self._init = True |
4544 | self._init_parent = {} |
4545 | self._assertion_report = assertion_report.assertion_report() |
4546 | @@ -97,10 +95,6 @@ |
4547 | """ Return an iterator over all model names. """ |
4548 | return iter(self.models) |
4549 | |
4550 | - def __contains__(self, model_name): |
4551 | - """ Test whether the model with the given name exists. """ |
4552 | - return model_name in self.models |
4553 | - |
4554 | def __getitem__(self, model_name): |
4555 | """ Return the model with the given name or raise KeyError if it doesn't exist.""" |
4556 | return self.models[model_name] |
4557 | @@ -109,6 +103,16 @@ |
4558 | """ Same as ``self[model_name]``. """ |
4559 | return self.models[model_name] |
4560 | |
4561 | + @lazy_property |
4562 | + def pure_function_fields(self): |
4563 | + """ Return the list of pure function fields (field objects) """ |
4564 | + fields = [] |
4565 | + for mname, fnames in self._pure_function_fields.iteritems(): |
4566 | + model_fields = self[mname]._fields |
4567 | + for fname in fnames: |
4568 | + fields.append(model_fields[fname]) |
4569 | + return fields |
4570 | + |
4571 | def do_parent_store(self, cr): |
4572 | for o in self._init_parent: |
4573 | self.get(o)._parent_store_compute(cr) |
4574 | @@ -132,14 +136,25 @@ |
4575 | |
4576 | """ |
4577 | models_to_load = [] # need to preserve loading order |
4578 | + lazy_property.reset_all(self) |
4579 | + |
4580 | + # call hook before adding stuff in the registry |
4581 | + for model in self.models.itervalues(): |
4582 | + model._before_registry_update(cr, SUPERUSER_ID) |
4583 | + |
4584 | # Instantiate registered classes (via the MetaModel automatic discovery |
4585 | # or via explicit constructor call), and add them to the pool. |
4586 | for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []): |
4587 | # models register themselves in self.models |
4588 | - model = cls.create_instance(self, cr) |
4589 | + model = cls._build_model(self, cr) |
4590 | if model._name not in models_to_load: |
4591 | # avoid double-loading models whose declaration is split |
4592 | models_to_load.append(model._name) |
4593 | + |
4594 | + # call hook after models have been instantiated |
4595 | + for model in self.models.itervalues(): |
4596 | + model._after_registry_update(cr, SUPERUSER_ID) |
4597 | + |
4598 | return [self.models[m] for m in models_to_load] |
4599 | |
4600 | def clear_caches(self): |
4601 | @@ -151,7 +166,7 @@ |
4602 | model.clear_caches() |
4603 | # Special case for ir_ui_menu which does not use openerp.tools.ormcache. |
4604 | ir_ui_menu = self.models.get('ir.ui.menu') |
4605 | - if ir_ui_menu: |
4606 | + if ir_ui_menu is not None: |
4607 | ir_ui_menu.clear_cache() |
4608 | |
4609 | |
4610 | @@ -282,36 +297,37 @@ |
4611 | """ |
4612 | import openerp.modules |
4613 | with cls.lock(): |
4614 | - registry = Registry(db_name) |
4615 | - |
4616 | - # Initializing a registry will call general code which will in turn |
4617 | - # call registries.get (this object) to obtain the registry being |
4618 | - # initialized. Make it available in the registries dictionary then |
4619 | - # remove it if an exception is raised. |
4620 | - cls.delete(db_name) |
4621 | - cls.registries[db_name] = registry |
4622 | - try: |
4623 | - with registry.cursor() as cr: |
4624 | - seq_registry, seq_cache = Registry.setup_multi_process_signaling(cr) |
4625 | - registry.base_registry_signaling_sequence = seq_registry |
4626 | - registry.base_cache_signaling_sequence = seq_cache |
4627 | - # This should be a method on Registry |
4628 | - openerp.modules.load_modules(registry._db, force_demo, status, update_module) |
4629 | - except Exception: |
4630 | - del cls.registries[db_name] |
4631 | - raise |
4632 | - |
4633 | - # load_modules() above can replace the registry by calling |
4634 | - # indirectly new() again (when modules have to be uninstalled). |
4635 | - # Yeah, crazy. |
4636 | - registry = cls.registries[db_name] |
4637 | - |
4638 | - cr = registry.cursor() |
4639 | - try: |
4640 | - registry.do_parent_store(cr) |
4641 | - cr.commit() |
4642 | - finally: |
4643 | - cr.close() |
4644 | + with openerp.Environment.manage(): |
4645 | + registry = Registry(db_name) |
4646 | + |
4647 | + # Initializing a registry will call general code which will in |
4648 | + # turn call registries.get (this object) to obtain the registry |
4649 | + # being initialized. Make it available in the registries |
4650 | + # dictionary then remove it if an exception is raised. |
4651 | + cls.delete(db_name) |
4652 | + cls.registries[db_name] = registry |
4653 | + try: |
4654 | + with registry.cursor() as cr: |
4655 | + seq_registry, seq_cache = Registry.setup_multi_process_signaling(cr) |
4656 | + registry.base_registry_signaling_sequence = seq_registry |
4657 | + registry.base_cache_signaling_sequence = seq_cache |
4658 | + # This should be a method on Registry |
4659 | + openerp.modules.load_modules(registry._db, force_demo, status, update_module) |
4660 | + except Exception: |
4661 | + del cls.registries[db_name] |
4662 | + raise |
4663 | + |
4664 | + # load_modules() above can replace the registry by calling |
4665 | + # indirectly new() again (when modules have to be uninstalled). |
4666 | + # Yeah, crazy. |
4667 | + registry = cls.registries[db_name] |
4668 | + |
4669 | + cr = registry.cursor() |
4670 | + try: |
4671 | + registry.do_parent_store(cr) |
4672 | + cr.commit() |
4673 | + finally: |
4674 | + cr.close() |
4675 | |
4676 | registry.ready = True |
4677 | |
4678 | |
4679 | === modified file 'openerp/osv/__init__.py' |
4680 | --- openerp/osv/__init__.py 2013-02-12 14:24:10 +0000 |
4681 | +++ openerp/osv/__init__.py 2014-05-19 13:53:27 +0000 |
4682 | @@ -19,9 +19,10 @@ |
4683 | # |
4684 | ############################################################################## |
4685 | |
4686 | +import api |
4687 | import osv |
4688 | +import env |
4689 | import fields |
4690 | - |
4691 | +import fields2 |
4692 | |
4693 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
4694 | - |
4695 | |
4696 | === added file 'openerp/osv/api.py' |
4697 | --- openerp/osv/api.py 1970-01-01 00:00:00 +0000 |
4698 | +++ openerp/osv/api.py 2014-05-19 13:53:27 +0000 |
4699 | @@ -0,0 +1,624 @@ |
4700 | +# -*- coding: utf-8 -*- |
4701 | +############################################################################## |
4702 | +# |
4703 | +# OpenERP, Open Source Management Solution |
4704 | +# Copyright (C) 2013 OpenERP (<http://www.openerp.com>). |
4705 | +# |
4706 | +# This program is free software: you can redistribute it and/or modify |
4707 | +# it under the terms of the GNU Affero General Public License as |
4708 | +# published by the Free Software Foundation, either version 3 of the |
4709 | +# License, or (at your option) any later version. |
4710 | +# |
4711 | +# This program is distributed in the hope that it will be useful, |
4712 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
4713 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
4714 | +# GNU Affero General Public License for more details. |
4715 | +# |
4716 | +# You should have received a copy of the GNU Affero General Public License |
4717 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
4718 | +# |
4719 | +############################################################################## |
4720 | + |
4721 | +""" This module provides the elements for managing two different API styles, |
4722 | + namely the "traditional" and "record" styles. |
4723 | + |
4724 | + In the "traditional" style, parameters like the database cursor, user id, |
4725 | + context dictionary and record ids (usually denoted as ``cr``, ``uid``, |
4726 | + ``context``, ``ids``) are passed explicitly to all methods. In the "record" |
4727 | + style, those parameters are hidden into model instances, which gives it a |
4728 | + more object-oriented feel. |
4729 | + |
4730 | + For instance, the statements:: |
4731 | + |
4732 | + model = self.pool.get(MODEL) |
4733 | + ids = model.search(cr, uid, DOMAIN, context=context) |
4734 | + for rec in model.browse(cr, uid, ids, context=context): |
4735 | + print rec.name |
4736 | + model.write(cr, uid, ids, VALUES, context=context) |
4737 | + |
4738 | + may also be written as:: |
4739 | + |
4740 | + env = Env(cr, uid, context) # cr, uid, context wrapped in env |
4741 | + recs = env[MODEL] # retrieve an instance of MODEL |
4742 | + recs = recs.search(DOMAIN) # search returns a recordset |
4743 | + for rec in recs: # iterate over the records |
4744 | + print rec.name |
4745 | + recs.write(VALUES) # update all records in recs |
4746 | + |
4747 | + Methods written in the "traditional" style are automatically decorated, |
4748 | + following some heuristics based on parameter names. |
4749 | +""" |
4750 | + |
4751 | +__all__ = [ |
4752 | + 'Meta', 'guess', 'noguess', |
4753 | + 'model', 'multi', 'one', |
4754 | + 'cr', 'cr_context', 'cr_uid', 'cr_uid_context', |
4755 | + 'cr_uid_id', 'cr_uid_id_context', 'cr_uid_ids', 'cr_uid_ids_context', |
4756 | + 'constrains', 'depends', 'onchange', 'returns', 'propagate_returns', |
4757 | +] |
4758 | + |
4759 | +from inspect import getargspec |
4760 | +import logging |
4761 | + |
4762 | +_logger = logging.getLogger(__name__) |
4763 | + |
4764 | + |
4765 | +# |
4766 | +# The following attributes are used, and reflected on wrapping methods: |
4767 | +# - method._api: decorator function, used for re-applying decorator |
4768 | +# - method._constrains: set by @constrains, specifies constraint dependencies |
4769 | +# - method._depends: set by @depends, specifies compute dependencies |
4770 | +# - method._returns: set by @returns, specifies return model |
4771 | +# - method.clear_cache: set by @ormcache, used to clear the cache |
4772 | +# |
4773 | +# On wrapping method only: |
4774 | +# - method._orig: original method |
4775 | +# |
4776 | + |
4777 | +_WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', |
4778 | + '_api', '_constrains', '_depends', '_returns', 'clear_cache') |
4779 | + |
4780 | + |
4781 | +class Meta(type): |
4782 | + """ Metaclass that automatically decorates traditional-style methods by |
4783 | + guessing their API. It also implements the inheritance of the |
4784 | + :func:`returns` decorators. |
4785 | + """ |
4786 | + |
4787 | + def __new__(meta, name, bases, attrs): |
4788 | + # dummy parent class to catch overridden methods decorated with 'returns' |
4789 | + parent = type.__new__(meta, name, bases, {}) |
4790 | + |
4791 | + for key, value in attrs.items(): |
4792 | + if not key.startswith('__') and callable(value): |
4793 | + # make the method inherit from @returns decorators |
4794 | + if not get_returns(value): |
4795 | + value = propagate_returns(getattr(parent, key, None), value) |
4796 | + |
4797 | + # guess calling convention if none is given |
4798 | + if not hasattr(value, '_api'): |
4799 | + try: |
4800 | + value = guess(value) |
4801 | + except TypeError: |
4802 | + pass |
4803 | + |
4804 | + attrs[key] = value |
4805 | + |
4806 | + return type.__new__(meta, name, bases, attrs) |
4807 | + |
4808 | + |
4809 | +def constrains(*args): |
4810 | + """ Return a decorator that specifies the field dependencies of a method |
4811 | + implementing a constraint checker. Each argument must be a field name. |
4812 | + """ |
4813 | + def decorate(method): |
4814 | + method._constrains = args |
4815 | + return method |
4816 | + |
4817 | + return decorate |
4818 | + |
4819 | + |
4820 | +def onchange(*args): |
4821 | + """ Return a decorator to decorate an onchange method for given fields. |
4822 | + Each argument must be a field name. |
4823 | + """ |
4824 | + def decorate(method): |
4825 | + method._onchange = args |
4826 | + return method |
4827 | + |
4828 | + return decorate |
4829 | + |
4830 | + |
4831 | +def depends(*args): |
4832 | + """ Return a decorator that specifies the field dependencies of a "compute" |
4833 | + method (for new-style function fields). Each argument must be a string |
4834 | + that consists in a dot-separated sequence of field names. |
4835 | + |
4836 | + One may also pass a single function as argument. In that case, the |
4837 | + dependencies are given by calling the function with the field's model. |
4838 | + """ |
4839 | + if args and callable(args[0]): |
4840 | + args = args[0] |
4841 | + |
4842 | + def decorate(method): |
4843 | + method._depends = args |
4844 | + return method |
4845 | + |
4846 | + return decorate |
4847 | + |
4848 | + |
4849 | +def returns(model, downgrade=None): |
4850 | + """ Return a decorator for methods that return instances of `model`. |
4851 | + |
4852 | + :param model: a model name, or ``'self'`` for the current model |
4853 | + |
4854 | + :param downgrade: a function `downgrade(value)` to convert the |
4855 | + record-style `value` to a traditional-style output |
4856 | + |
4857 | + The decorator adapts the method output to the api style: `id`, `ids` or |
4858 | + ``False`` for the traditional style, and recordset for the record style:: |
4859 | + |
4860 | + @model |
4861 | + @returns('res.partner') |
4862 | + def find_partner(self, arg): |
4863 | + ... # return some record |
4864 | + |
4865 | + # output depends on call style: traditional vs record style |
4866 | + partner_id = model.find_partner(cr, uid, arg, context=context) |
4867 | + |
4868 | + # recs = model.browse(cr, uid, ids, context) |
4869 | + partner_record = recs.find_partner(arg) |
4870 | + |
4871 | + Note that the decorated method must satisfy that convention. |
4872 | + |
4873 | + Those decorators are automatically *inherited*: a method that overrides |
4874 | + a decorated existing method will be decorated with the same |
4875 | + ``@returns(model)``. |
4876 | + """ |
4877 | + def decorate(method): |
4878 | + if hasattr(method, '_orig'): |
4879 | + # decorate the original method, and re-apply the api decorator |
4880 | + origin = method._orig |
4881 | + origin._returns = model, downgrade |
4882 | + return origin._api(origin) |
4883 | + else: |
4884 | + method._returns = model, downgrade |
4885 | + return method |
4886 | + |
4887 | + return decorate |
4888 | + |
4889 | + |
4890 | +def get_returns(method): |
4891 | + return getattr(method, '_returns', None) |
4892 | + |
4893 | + |
4894 | +def propagate_returns(from_method, to_method): |
4895 | + spec = get_returns(from_method) |
4896 | + if spec: |
4897 | + _logger.debug("Method %s.%s inherited @returns%r", |
4898 | + to_method.__module__, to_method.__name__, spec) |
4899 | + return returns(*spec)(to_method) |
4900 | + else: |
4901 | + return to_method |
4902 | + |
4903 | + |
4904 | +def make_wrapper(method, old_api, new_api): |
4905 | + """ Return a wrapper method for `method`. """ |
4906 | + def wrapper(self, *args, **kwargs): |
4907 | + # avoid hasattr(self, '_ids') because __getattr__() is overridden |
4908 | + if '_ids' in self.__dict__: |
4909 | + return new_api(self, *args, **kwargs) |
4910 | + else: |
4911 | + return old_api(self, *args, **kwargs) |
4912 | + |
4913 | + # propagate specific openerp attributes to wrapper |
4914 | + for attr in _WRAPPED_ATTRS: |
4915 | + if hasattr(method, attr): |
4916 | + setattr(wrapper, attr, getattr(method, attr)) |
4917 | + wrapper._orig = method |
4918 | + |
4919 | + return wrapper |
4920 | + |
4921 | + |
4922 | +def get_downgrade(method): |
4923 | + """ Return a function `downgrade(value)` that adapts `value` from |
4924 | + record-style to traditional-style, following the convention of `method`. |
4925 | + """ |
4926 | + spec = get_returns(method) |
4927 | + if spec: |
4928 | + model, downgrade = spec |
4929 | + return downgrade or (lambda value: value.ids) |
4930 | + else: |
4931 | + return lambda value: value |
4932 | + |
4933 | + |
4934 | +def get_upgrade(method): |
4935 | + """ Return a function `upgrade(self, value)` that adapts `value` from |
4936 | + traditional-style to record-style, following the convention of `method`. |
4937 | + """ |
4938 | + spec = get_returns(method) |
4939 | + if spec: |
4940 | + model, downgrade = spec |
4941 | + if model == 'self': |
4942 | + return lambda self, value: self.browse(value) |
4943 | + else: |
4944 | + return lambda self, value: self.env[model].browse(value) |
4945 | + else: |
4946 | + return lambda self, value: value |
4947 | + |
4948 | + |
4949 | +def get_aggregate(method): |
4950 | + """ Return a function `aggregate(self, value)` that aggregates record-style |
4951 | + `value` for a method decorated with ``@one``. |
4952 | + """ |
4953 | + spec = get_returns(method) |
4954 | + if spec: |
4955 | + # value is a list of instances, concatenate them |
4956 | + model, downgrade = spec |
4957 | + if model == 'self': |
4958 | + return lambda self, value: sum(value, self.browse()) |
4959 | + else: |
4960 | + return lambda self, value: sum(value, self.env[model].browse()) |
4961 | + else: |
4962 | + return lambda self, value: value |
4963 | + |
4964 | + |
4965 | +def get_context_split(method): |
4966 | + """ Return a function `split` that extracts the context from a pair of |
4967 | + positional and keyword arguments:: |
4968 | + |
4969 | + context, args, kwargs = split(args, kwargs) |
4970 | + """ |
4971 | + pos = len(getargspec(method).args) - 1 |
4972 | + |
4973 | + def split(args, kwargs): |
4974 | + if pos < len(args): |
4975 | + return args[pos], args[:pos], kwargs |
4976 | + else: |
4977 | + return kwargs.pop('context', None), args, kwargs |
4978 | + |
4979 | + return split |
4980 | + |
4981 | + |
4982 | +def model(method): |
4983 | + """ Decorate a record-style method where `self` is a recordset. Such a |
4984 | + method:: |
4985 | + |
4986 | + @api.model |
4987 | + def method(self, args): |
4988 | + ... |
4989 | + |
4990 | + may be called in both record and traditional styles, like:: |
4991 | + |
4992 | + # recs = model.browse(cr, uid, ids, context) |
4993 | + recs.method(args) |
4994 | + |
4995 | + model.method(cr, uid, args, context=context) |
4996 | + """ |
4997 | + method._api = model |
4998 | + split = get_context_split(method) |
4999 | + downgrade = get_downgrade(method) |
5000 | + |
First (global) look: addons/ base/ir/ ir_translation. py: why removing the check of the existence of the record? import. py: same here, a scope for the whole file.
openerp/
tools/translate.py: as there is always a scope why not rewrite _() to use current scope instead of using the stack to find the lang... (if no scope, just return the input)?
tools/convert.py: why create a new scope for each tag? a scope for the whole file is enough. NOTE: IIRC a tag <record> can use another uid.
tools/yaml_
osv/fields.py: in get() methods, no need to create a new scope, current scope is ok.
osv/scope.py: I don't like the "proxy" variable name. what's wrong with scope (except the easy confusion with Scope)
osv/orm.py: not yet review...