diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 75f6459929b..5a60e1c17a8 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -13,7 +13,7 @@ -
+
diff --git a/addons/base_import_module/models/__init__.py b/addons/base_import_module/models/__init__.py index 07d35e729c2..18a246fa151 100644 --- a/addons/base_import_module/models/__init__.py +++ b/addons/base_import_module/models/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- +# flake8: noqa import base_import_module import ir_module +import ir_ui_view diff --git a/addons/base_import_module/models/ir_ui_view.py b/addons/base_import_module/models/ir_ui_view.py new file mode 100644 index 00000000000..af0b13bdfd1 --- /dev/null +++ b/addons/base_import_module/models/ir_ui_view.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from operator import itemgetter +from openerp import api, models + +class IrUiView(models.Model): + _inherit = 'ir.ui.view' + + @api.model + def _validate_custom_views(self, model): + # views from imported modules should be considered as custom views + result = super(IrUiView, self)._validate_custom_views(model) + + self._cr.execute(""" + SELECT max(v.id) + FROM ir_ui_view v + LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) + LEFT JOIN ir_module_module m ON (m.name = md.module) + WHERE m.imported = true + AND v.model = %s + AND v.active = true + GROUP BY coalesce(v.inherit_id, v.id) + """, [model]) + + ids = map(itemgetter(0), self._cr.fetchall()) + views = self.with_context(load_all_views=True).browse(ids) + return views._check_xml() and result diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index fe52be16430..eb69a0437b1 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -207,7 +207,7 @@ class Employee(models.Model): today_date = datetime.datetime.utcnow().date() today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59)) - holidays = self.env['hr.holidays'].search([ + holidays = self.env['hr.holidays'].sudo().search([ ('employee_id', '!=', False), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py index 5df19a7e3fb..dfa1dbf859b 100644 --- a/addons/pad/models/pad.py +++ b/addons/pad/models/pad.py @@ -80,7 +80,7 @@ class PadCommon(models.AbstractModel): if url: try: page = urllib2.urlopen('%s/export/html' % url).read() - mo = re.search('(.*)', page) + mo = re.search('(.*)', page, re.DOTALL) if mo: content = mo.group(1) except: diff --git a/addons/sale/report/invoice_report_templates.xml b/addons/sale/report/invoice_report_templates.xml index 3f615241e15..ef26e950a8f 100644 --- a/addons/sale/report/invoice_report_templates.xml +++ b/addons/sale/report/invoice_report_templates.xml @@ -43,7 +43,7 @@ - + https://github.com/PinZhang diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index c7615248c29..fcd24de2f79 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -649,7 +649,7 @@ class QWeb(object): if hasattr(self, interpret_handler): _logger.warning( "Directive '%s' must be AST-compiled. Dynamic interpreter %s will ignored", - name, interpret_handler + mname, interpret_handler ) return compile_handler(el, options) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 039cbf1bd7c..5c74c83b9a9 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -1111,7 +1111,7 @@ actual arch. query = """SELECT max(v.id) FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) - WHERE md.module IS NULL + WHERE md.module NOT IN (SELECT name FROM ir_module_module) AND v.model = %s AND v.active = true GROUP BY coalesce(v.inherit_id, v.id)""" diff --git a/odoo/addons/base/res/res_currency_data.xml b/odoo/addons/base/res/res_currency_data.xml index c89552c5783..22478fbcfa2 100644 --- a/odoo/addons/base/res/res_currency_data.xml +++ b/odoo/addons/base/res/res_currency_data.xml @@ -71,7 +71,7 @@ HUF Ft - 0.01 + 1.00 diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index 8616e7c47bb..c8bcaa042ad 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -351,10 +351,12 @@ class Partner(models.Model): result['value'] = {key: convert(self.parent_id[key]) for key in address_fields} return result - @api.onchange('state_id') - def onchange_state(self): - if self.state_id: - self.country_id = self.state_id.country_id + @api.onchange('country_id') + def _onchange_country_id(self): + if self.country_id: + return {'domain': {'state_id': [('country_id', '=', self.country_id.id)]}} + else: + return {'domain': {'state_id': []}} @api.onchange('email') def onchange_email(self): diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 60e2760c7c9..932268d717c 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -241,6 +241,9 @@ class TestExpression(TransactionCase): partners = Partner.search([('company_id.partner_id', 'in', [])]) self.assertFalse(partners) + # check multi-level expressions with magic columns + partners = Partner.search([('create_uid.active', '=', True)]) + # check that multi-level expressions with negative op work all_partners = Partner.search([('company_id', '!=', False)]) res_partners = Partner.search([('company_id.partner_id', 'not in', [])]) diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py index f379f55d3c0..7841b23fd0e 100644 --- a/odoo/modules/loading.py +++ b/odoo/modules/loading.py @@ -191,6 +191,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= # Set new modules and dependencies module.write({'state': 'installed', 'latest_version': ver}) + package.load_state = package.state + package.load_version = package.installed_version package.state = 'installed' for kind in ('init', 'demo', 'update'): if hasattr(package, kind): @@ -338,6 +340,11 @@ def load_modules(db, force_demo=False, status=None, update_module=False): registry.setup_models(cr) + # STEP 3.5: execute migration end-scripts + migrations = odoo.modules.migration.MigrationManager(cr, graph) + for package in graph: + migrations.migrate_module(package, 'end') + # STEP 4: Finish and cleanup installations if processed_modules: cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") @@ -394,12 +401,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # STEP 6: verify custom views on every model if update_module: View = env['ir.ui.view'] - custom_view_test = True for model in registry: if not View._validate_custom_views(model): - custom_view_test = False - _logger.error('invalid custom view(s) for model %s', model) - report.record_result(custom_view_test) + _logger.warning('invalid custom view(s) for model %s', model) if report.failures: _logger.error('At least one test failed when loading the modules.') diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 67f1d4d5c89..552c9d8dae7 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -3,12 +3,14 @@ """ Modules migration handling. """ +from collections import defaultdict +import glob import imp import logging import os from os.path import join as opj -import odoo +from odoo.modules.module import get_resource_path import odoo.release as release import odoo.tools as tools from odoo.tools.parse_version import parse_version @@ -19,59 +21,64 @@ _logger = logging.getLogger(__name__) class MigrationManager(object): """ This class manage the migration of modules - Migrations files must be python files containing a "migrate(cr, installed_version)" function. - Theses files must respect a directory tree structure: A 'migrations' folder which containt a - folder by version. Version can be 'module' version or 'server.module' version (in this case, - the files will only be processed by this version of the server). Python file names must start - by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation + Migrations files must be python files containing a `migrate(cr, installed_version)` + function. Theses files must respect a directory tree structure: A 'migrations' folder + which containt a folder by version. Version can be 'module' version or 'server.module' + version (in this case, the files will only be processed by this version of the server). + Python file names must start by `pre` or `post` and will be executed, respectively, + before and after the module initialisation. `end` scripts are run after all modules have + been updated. Example: - `-- migrations |-- 1.0 | |-- pre-update_table_x.py | |-- pre-update_table_y.py - | |-- post-clean-data.py - | `-- README.txt # not processed - |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server + | |-- post-create_plop_records.py + | |-- end-cleanup.py + | `-- README.txt # not processed + |-- 9.0.1.1 # processed only on a 9.0 server | |-- pre-delete_table_z.py | `-- post-clean-data.py - `-- foo.py # not processed - - This similar structure is generated by the maintenance module with the migrations files get by - the maintenance contract - + `-- foo.py # not processed """ + def __init__(self, cr, graph): self.cr = cr self.graph = graph - self.migrations = {} + self.migrations = defaultdict(dict) self._get_files() def _get_files(self): - - """ - import addons.base.maintenance.utils as maintenance_utils - maintenance_utils.update_migrations_files(self.cr) - #""" + def get_scripts(path): + if not path: + return {} + return { + version: glob.glob1(opj(path, version), '*.py') + for version in os.listdir(path) + if os.path.isdir(opj(path, version)) + } for pkg in self.graph: - self.migrations[pkg.name] = {} - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): + if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade' or + getattr(pkg, 'load_state', None) == 'to upgrade'): continue - get_module_filetree = odoo.modules.module.get_module_filetree - self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} - self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} + self.migrations[pkg.name] = { + 'module': get_scripts(get_resource_path(pkg.name, 'migrations')), + 'maintenance': get_scripts(get_resource_path('base', 'maintenance', 'migrations', pkg.name)), + } def migrate_module(self, pkg, stage): - assert stage in ('pre', 'post') + assert stage in ('pre', 'post', 'end') stageformat = { 'pre': '[>%s]', 'post': '[%s>]', + 'end': '[$%s]', } + state = pkg.state if stage in ('pre', 'post') else getattr(pkg, 'load_state', None) - if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade') or pkg.state == 'to install': + if not (hasattr(pkg, 'update') or state == 'to upgrade') or state == 'to install': return def convert_version(version): @@ -80,18 +87,17 @@ class MigrationManager(object): return "%s.%s" % (release.major_version, version) def _get_migration_versions(pkg): - def __get_dir(tree): - return [d for d in tree if tree[d] is not None] - versions = list(set( - __get_dir(self.migrations[pkg.name]['module']) + - __get_dir(self.migrations[pkg.name]['maintenance']) + ver + for lv in self.migrations[pkg.name].values() + for ver, lf in lv.items() + if lf )) versions.sort(key=lambda k: parse_version(convert_version(k))) return versions def _get_migration_files(pkg, version, stage): - """ return a list of tuple (module, file) + """ return a list of migration script files """ m = self.migrations[pkg.name] lst = [] @@ -102,17 +108,15 @@ class MigrationManager(object): } for x in mapping.keys(): - if version in m[x]: + if version in m.get(x): for f in m[x][version]: - if m[x][version][f] is not None: - continue if not f.startswith(stage + '-'): continue lst.append(opj(mapping[x], version, f)) lst.sort() return lst - parsed_installed_version = parse_version(pkg.installed_version or '') + parsed_installed_version = parse_version(getattr(pkg, 'load_version', pkg.installed_version) or '') current_version = parse_version(convert_version(pkg.data['version'])) versions = _get_migration_versions(pkg) diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index a5b753f322c..b698ccfcb3a 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -836,9 +836,6 @@ class expression(object): new_leaf = create_substitution_leaf(leaf, dom_leaf, model) push(new_leaf) - elif path[0] in MAGIC_COLUMNS: - push_result(leaf) - # ---------------------------------------- # PATH SPOTTED # -> many2one or one2many with _auto_join: @@ -1134,8 +1131,8 @@ class expression(object): # final sanity checks - should never fail assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \ "Invalid operator %r in domain term %r" % (operator, leaf) - assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields \ - or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf) + assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields, \ + "Invalid field %r in domain term %r" % (left, leaf) assert not isinstance(right, BaseModel), \ "Invalid value %r in domain term %r" % (right, leaf)