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

Proposed by Raphael Collet (OpenERP)
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
Reviewer Review Type Date Requested Status
Christophe Simonis (OpenERP) Needs Fixing
Review via email: mp+190975@code.launchpad.net

Description of the change

The new API

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

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

5108. By Raphael Collet (OpenERP)

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

5109. By Raphael Collet (OpenERP)

[MERGE] from trunk

5110. By Raphael Collet (OpenERP)

[IMP] fields2: cleanup of Field.interface

5111. By Raphael Collet (OpenERP)

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

5112. By Raphael Collet (OpenERP)

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

5113. By Raphael Collet (OpenERP)

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

5114. By Raphael Collet (OpenERP)

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

5115. By Raphael Collet (OpenERP)

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

5116. By Raphael Collet (OpenERP)

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

5117. By Raphael Collet (OpenERP)

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

5118. By Stephane Wirtel (OpenERP)

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

5119. By Stephane Wirtel (OpenERP)

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

5120. By Raphael Collet (OpenERP)

[IMP] ir_ui_view: improve method name

5121. By Stephane Wirtel (OpenERP)

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

5122. By Raphael Collet (OpenERP)

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

5123. By Stephane Wirtel (OpenERP)

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

5124. By Raphael Collet (OpenERP)

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

5125. By Raphael Collet (OpenERP)

[IMP] orm: refactor method create_instance()

5126. By Raphael Collet (OpenERP)

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

5127. By Raphael Collet (OpenERP)

[IMP] scope: small code refactoring

5128. By Raphael Collet (OpenERP)

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

5129. By Raphael Collet (OpenERP)

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

5130. By Raphael Collet (OpenERP)

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

5131. By Stephane Wirtel (OpenERP)

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

5132. By Stephane Wirtel (OpenERP)

[FIX] test_mail: Rewrite the XSS test

5133. By Raphael Collet (OpenERP)

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

5134. By Raphael Collet (OpenERP)

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

5135. By Raphael Collet (OpenERP)

[MERGE] from trunk

5136. By Raphael Collet (OpenERP)

[MERGE] from trunk

5137. By Raphael Collet (OpenERP)

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

5138. By Stephane Wirtel (OpenERP)

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

5139. By Stephane Wirtel (OpenERP)

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

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

5140. By Stephane Wirtel (OpenERP)

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

The previous implementation was wrong! Sorry

5141. By Stephane Wirtel (OpenERP)

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

5142. By Raphael Collet (OpenERP)

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

5143. By Raphael Collet (OpenERP)

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

5144. By Raphael Collet (OpenERP)

[FIX] res_company: fix call to read()

5145. By Raphael Collet (OpenERP)

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

5146. By Raphael Collet (OpenERP)

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

5147. By Raphael Collet (OpenERP)

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

5148. By Stephane Wirtel (OpenERP)

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

5149. By Stephane Wirtel (OpenERP)

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

5150. By Raphael Collet (OpenERP)

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

5151. By Raphael Collet (OpenERP)

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

5152. By Raphael Collet (OpenERP)

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

5153. By Raphael Collet (OpenERP)

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

5154. By Raphael Collet (OpenERP)

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

5155. By Raphael Collet (OpenERP)

[FIX] fields2: undefined variable

5156. By Raphael Collet (OpenERP)

[FIX] base: missing dependency on computed field

5157. By Raphael Collet (OpenERP)

[MERGE] from trunk

5158. By Raphael Collet (OpenERP)

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

5159. By Raphael Collet (OpenERP)

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

5160. By Raphael Collet (OpenERP)

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

5161. By Raphael Collet (OpenERP)

[FIX] fields: add missing comma in tuple

5162. By Raphael Collet (OpenERP)

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

5163. By Raphael Collet (OpenERP)

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

5164. By Raphael Collet (OpenERP)

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

5165. By Raphael Collet (OpenERP)

[IMP] orm: improve method check_field_access_rights()

5166. By Raphael Collet (OpenERP)

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

5167. By Raphael Collet (OpenERP)

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

5168. By Stephane Wirtel (OpenERP)

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

5169. By Stephane Wirtel (OpenERP)

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

5170. By Raphael Collet (OpenERP)

[MERGE] from trunk

5171. By Raphael Collet (OpenERP)

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

5172. By Stephane Wirtel (OpenERP)

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

5173. By Raphael Collet (OpenERP)

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

5174. By Raphael Collet (OpenERP)

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

5175. By Raphael Collet (OpenERP)

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

5176. By Raphael Collet (OpenERP)

[IMP] api: make scope getter function more specific

5177. By Raphael Collet (OpenERP)

[MERGE] from trunk

5178. By Stephane Wirtel (OpenERP)

[MERGE] from trunk

5179. By Raphael Collet (OpenERP)

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

5180. By Raphael Collet (OpenERP)

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

5181. By Raphael Collet (OpenERP)

[MERGE] from trunk

5182. By Raphael Collet (OpenERP)

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

5183. By Raphael Collet (OpenERP)

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

5184. By Raphael Collet (OpenERP)

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

5185. By Raphael Collet (OpenERP)

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

5186. By Raphael Collet (OpenERP)

[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

Revision history for this message
Christophe Simonis (OpenERP) (kangol) wrote :

First (global) look:
openerp/addons/base/ir/ir_translation.py: why removing the check of the existence of the record?
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_import.py: same here, a scope for the whole file.
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...

review: Needs Fixing
Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/03_module_dev_02.rst'
2--- doc/03_module_dev_02.rst 2013-06-19 09:13:32 +0000
3+++ doc/03_module_dev_02.rst 2014-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+
The diff has been truncated for viewing.