From f984073251efe2f1ca35fb520139f268f91e86aa Mon Sep 17 00:00:00 2001 From: Daniel Dico Date: Thu, 29 Dec 2016 18:14:58 -0500 Subject: [PATCH 1/9] [FIX] pad: get pad content with newlines Some more recent versions of etherpad can have newlines in the pad content. Allowing newlines should not break anything that worked before and allow more etherpad versions to be used. closes #14877 opw-703173 --- addons/pad/pad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/pad/pad.py b/addons/pad/pad.py index 711bbd46fc5..286c80d203a 100644 --- a/addons/pad/pad.py +++ b/addons/pad/pad.py @@ -75,7 +75,7 @@ class pad_common(osv.osv_memory): 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: From fc91feda3033f040b918fcfa854029d90c897f34 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 28 Nov 2016 13:23:01 +0100 Subject: [PATCH 2/9] [IMP] core: more lenient check of custom views By not marking invalid custom views as a failure, this allow database migrations to not be considered failed for simple errors that can be solved after migration. --- openerp/addons/base/ir/ir_ui_view.py | 2 +- openerp/modules/loading.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 707b81e213e..8d49c129947 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -1163,7 +1163,7 @@ class view(osv.osv): 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) - 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/openerp/modules/loading.py b/openerp/modules/loading.py index 18592b511cd..174b9b7c7a9 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -394,12 +394,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # STEP 6: verify custom views on every model if update_module: Views = registry['ir.ui.view'] - custom_view_test = True for model in registry.models.keys(): if not Views._validate_custom_views(cr, SUPERUSER_ID, 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.') From 1aad733dc4b4d9b4e69b0ef8236f5f20c5510d2d Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 28 Nov 2016 14:09:30 +0100 Subject: [PATCH 3/9] [IMP] base_import_module: correct validation of custom views Consider also imported and exported views when validating custom views. --- addons/base_import_module/models/__init__.py | 2 ++ .../base_import_module/models/ir_ui_view.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 addons/base_import_module/models/ir_ui_view.py diff --git a/addons/base_import_module/models/__init__.py b/addons/base_import_module/models/__init__.py index 02b8831509a..f03f6c5ce87 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 ir_module import base_import_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 From 4ea61f6c54d69f2f06d2df2863abbcb33b22d410 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Mon, 2 Jan 2017 16:57:28 +0100 Subject: [PATCH 4/9] [FIX] sale_layout: unify tax displayed field in report Use the same logic to display taxes as in the default report: use the tax description and fallback on the name Already done in report_invoice_document and report_saleorder_document Closes #14906 --- addons/sale_layout/views/report_invoice_layouted.xml | 2 +- addons/sale_layout/views/report_quotation_layouted.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sale_layout/views/report_invoice_layouted.xml b/addons/sale_layout/views/report_invoice_layouted.xml index 06d98dca4b1..513354b3bb5 100644 --- a/addons/sale_layout/views/report_invoice_layouted.xml +++ b/addons/sale_layout/views/report_invoice_layouted.xml @@ -22,7 +22,7 @@ - + - + Date: Mon, 2 Jan 2017 17:06:55 +0100 Subject: [PATCH 5/9] [FIX] sale_layout: correct the alignment of taxes To do the same on the rapport as what was done at #14511 --- addons/sale_layout/views/report_invoice_layouted.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_layout/views/report_invoice_layouted.xml b/addons/sale_layout/views/report_invoice_layouted.xml index 513354b3bb5..4cac735702b 100644 --- a/addons/sale_layout/views/report_invoice_layouted.xml +++ b/addons/sale_layout/views/report_invoice_layouted.xml @@ -21,7 +21,7 @@ - + From bbc413760029c0b190e58485de87adffc57b82be Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Tue, 3 Jan 2017 15:08:07 +0100 Subject: [PATCH 6/9] [FIX] calendar: partner filter for search more or created When adding a partner "filter" to the calendar view, it only works if we select directly the partner and not when we choose him via "search more" or create him on the fly. This commit uses the partner id directly which is available in the field value in those three possible instances. opw-702491 --- addons/calendar/static/src/js/base_calendar.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 64d7d084557..e6dad933f81 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -148,12 +148,12 @@ widgets.SidebarFilter.include({ .filter([["id", "=",this.view.dataset.context.uid]]) .first() .done(function(result){ - $.map(self.ir_model_m2o.display_value, function(element,index) { - if (result.partner_id[0] != index){ - self.ds_message = new data.DataSetSearch(self, 'calendar.contacts'); - defs.push(self.ds_message.call("create", [{'partner_id': index}])); - } - }); + var partner_id = self.ir_model_m2o.get_value(); + // don't duplicate [Me] filter for current user + if (result.partner_id[0] != partner_id){ + self.ds_message = new data.DataSetSearch(self, 'calendar.contacts'); + defs.push(self.ds_message.call("create", [{'partner_id': partner_id}])); + } })); return $.when.apply(null, defs).then(function() { return reload_favorite_list(self); From d619ad464f886b848ba48832ddeb20af4735d9a8 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 3 Jan 2017 15:54:09 +0100 Subject: [PATCH 7/9] [FIX] sale_service: update SO lines To reproduce: - Create 2 products with 'Track Service' set as 'Create a task and track hours'. - On a SO, add these 2 products and confirm (2 tasks will be created) - Go to Timesheet > Activities, and create a line for Task 1. The delivered quantity of Product 1 in the SO should be modified accordingly. - Change the activity line by setting 'Task 2' instead of 'Task 1'. The delivered quantity of Product 2 is updated accordingly, but the delivered quantity of Product 1 is unchanged. This is a complement of commit 4e60d28d00550. opw-693520 opw-695087 --- addons/sale_service/models/timesheet.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/sale_service/models/timesheet.py b/addons/sale_service/models/timesheet.py index d9de3af5e9c..f98481176fc 100644 --- a/addons/sale_service/models/timesheet.py +++ b/addons/sale_service/models/timesheet.py @@ -68,6 +68,12 @@ class account_analytic_line(models.Model): @api.multi def write(self, values): + so_lines = self.mapped('so_line') self._update_values(values) result = super(account_analytic_line, self).write(values) + + # Update delivered quantity on SO lines which are not linked to the analytic lines anymore + so_lines -= self.mapped('so_line') + if so_lines: + so_lines.with_context(force_so_lines=so_lines).sudo()._compute_analytic() return result From b201da493d5131834ff2fb50b9d66d02e3fcbd9f Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 4 Jan 2017 14:31:17 +0100 Subject: [PATCH 8/9] [FIX] sale: Ukrainian translation instead of English (UK) opw-702022 --- addons/sale/i18n/en_GB.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/i18n/en_GB.po b/addons/sale/i18n/en_GB.po index b8dd8d0a08e..af18b47597f 100644 --- a/addons/sale/i18n/en_GB.po +++ b/addons/sale/i18n/en_GB.po @@ -596,7 +596,7 @@ msgstr "" #. module: sale #: model:ir.model.fields,field_description:sale.field_sale_order_validity_date msgid "Expiration Date" -msgstr "Дата окончания срока действия" +msgstr "" #. module: sale #: model:ir.ui.view,arch_db:sale.view_order_product_search From cbff8629fa0d0e7b1f4d2847b01d86893ba91b77 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 29 Nov 2016 18:43:50 +0100 Subject: [PATCH 9/9] [IMP] core: allow declaration of `end` migration scripts These `end` migration script will be executed when all modules have been updated and models setup. Also accelerate the search of migration scripts, speeding up registry loading. --- openerp/modules/loading.py | 7 ++++ openerp/modules/migration.py | 79 ++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 174b9b7c7a9..985ba53cb8f 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -193,6 +193,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= # Set new modules and dependencies modobj.write(cr, SUPERUSER_ID, [module_id], {'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): @@ -339,6 +341,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 = openerp.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)""") diff --git a/openerp/modules/migration.py b/openerp/modules/migration.py index 9776a6816ad..78e2c1e55a8 100644 --- a/openerp/modules/migration.py +++ b/openerp/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 openerp +from openerp.modules.module import get_resource_path import openerp.release as release import openerp.tools as tools from openerp.tools.parse_version import parse_version @@ -19,11 +21,14 @@ _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: @@ -31,47 +36,52 @@ class MigrationManager(object): |-- 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 = openerp.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 +90,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 +111,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)