diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-07-23 16:03:25 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-07-23 16:03:25 +0200 |
| commit | 4e76173a430089c242b9cda35b70b6c833dad8b9 (patch) | |
| tree | ee183f9c5cace15a0705430a580909c2ed8f013e | |
| parent | 78a6124d5ab4eb0b44fdcbd455d9f322d18b8a70 (diff) | |
| parent | 219d2296d67488855891573c979857c5fbfd1ebe (diff) | |
[MERGE] forward port branch 11.0 up to 219d2296d67
29 files changed, 347 insertions, 59 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index c8a05a31a01..d21c96a4ea5 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1799,15 +1799,15 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, }) - if newly_created_move: - if move_date > (self.company_id.period_lock_date or '0000-00-00') and newly_created_move.date != move_date: - # The move date should be the maximum date between payment and invoice (in case - # of payment in advance). However, we should make sure the move date is not - # recorded before the period lock date as the tax statement for this period is - # probably already sent to the estate. - newly_created_move.write({'date': move_date}) - # post move - newly_created_move.post() + if newly_created_move: + if move_date > (self.company_id.period_lock_date or '0000-00-00') and newly_created_move.date != move_date: + # The move date should be the maximum date between payment and invoice (in case + # of payment in advance). However, we should make sure the move date is not + # recorded before the period lock date as the tax statement for this period is + # probably already sent to the estate. + newly_created_move.write({'date': move_date}) + # post move + newly_created_move.post() def _create_tax_basis_move(self): # Check if company_journal for cash basis is set if not, raise exception diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 974c3acb0fb..bb25cae9082 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -509,7 +509,7 @@ class AccountTaxTemplate(models.Model): amount_type = fields.Selection(default='percent', string="Tax Computation", required=True, selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')]) active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.") - company_id = fields.Many2one('res.company', string='Company') + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) children_tax_ids = fields.Many2many('account.tax.template', 'account_tax_template_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes') sequence = fields.Integer(required=True, default=1, help="The sequence field is used to define order in which the tax lines are applied.") diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 2353ec6fd7a..b4924a0caea 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -1,3 +1,4 @@ +from odoo import api from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.tests import tagged import time diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py index 1ca0178ba20..1bdecd6acc3 100644 --- a/addons/base_address_extended/models/base_address_extended.py +++ b/addons/base_address_extended/models/base_address_extended.py @@ -131,6 +131,12 @@ class Partner(models.Model): for k, v in vals.items(): partner[k] = v + def write(self, vals): + res = super(Partner, self).write(vals) + if 'country_id' in vals: + self._set_street() + return res + class Company(models.Model): _inherit = 'res.company' diff --git a/addons/base_address_extended/tests/test_street_fields.py b/addons/base_address_extended/tests/test_street_fields.py index d5817070ffd..cf5598b624f 100644 --- a/addons/base_address_extended/tests/test_street_fields.py +++ b/addons/base_address_extended/tests/test_street_fields.py @@ -51,5 +51,5 @@ class TestStreetFields(TransactionCase): self.write_and_assert(p1, {'street': 'Chaussee de Namur'}, 'Chaussee de Namur', 'Chaussee de Namur', '', '') self.write_and_assert(p1, {'street_name': 'Chee de Namur', 'street_number': '40'}, 'Chee de Namur, 40', 'Chee de Namur', '40', '') self.write_and_assert(p1, {'street_number2': '4'}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') - #we don't recompute the street fields when we change the country - self.write_and_assert(p1, {'country_id': self.env.ref('base.us').id}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') + #we do recompute the street fields when we change the country + self.write_and_assert(p1, {'country_id': self.env.ref('base.us').id}, '40/4 Chee de Namur', 'Chee de Namur', '40', '4') diff --git a/addons/base_sparse_field/models/models.py b/addons/base_sparse_field/models/models.py index ef5fc4e5dbf..77ea36a6bd8 100644 --- a/addons/base_sparse_field/models/models.py +++ b/addons/base_sparse_field/models/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import UserError class IrModelFields(models.Model): diff --git a/addons/im_support/controllers/i18n/sw.po b/addons/im_support/controllers/i18n/sw.po new file mode 100644 index 00000000000..7d75d59d753 --- /dev/null +++ b/addons/im_support/controllers/i18n/sw.po @@ -0,0 +1,155 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_planner +# +# Translators: +# Mpasua Msonobari <msonobari@gmail.com>, 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.saas~18\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-09-20 09:54+0000\n" +"PO-Revision-Date: 2017-09-20 09:54+0000\n" +"Last-Translator: Mpasua Msonobari <msonobari@gmail.com>, 2018\n" +"Language-Team: Swahili (https://www.transifex.com/odoo/teams/41243/sw/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sw\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_active +#: model:ir.ui.view,arch_db:web_planner.web_planner_view_search +msgid "Active" +msgstr "Amilifu" + +#. module: web_planner +#. openerp-web +#: code:addons/web_planner/static/src/js/web_planner_common.js:379 +#, python-format +msgid "Cancel" +msgstr "Katiza" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_create_uid +msgid "Created by" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_create_date +msgid "Created on" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_data +msgid "Data" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_display_name +msgid "Display Name" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_id +msgid "ID" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,help:web_planner.field_web_planner_active +msgid "" +"If the active field is set to False, it will allow you to hide the planner. " +"This change requires a refresh of your page." +msgstr "" + +#. module: web_planner +#: model:ir.ui.view,arch_db:web_planner.web_planner_view_search +msgid "Inactive" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner___last_update +msgid "Last Modified on" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_write_date +msgid "Last Updated on" +msgstr "" + +#. module: web_planner +#. openerp-web +#: code:addons/web_planner/static/src/js/web_planner_common.js:133 +#, python-format +msgid "Mark As Done" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_menu_id +msgid "Menu" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_name +msgid "Name" +msgstr "" + +#. module: web_planner +#. openerp-web +#: code:addons/web_planner/static/src/js/web_planner_common.js:141 +#, python-format +msgid "Next Step" +msgstr "" + +#. module: web_planner +#. openerp-web +#: code:addons/web_planner/static/src/js/web_planner_common.js:388 +#, python-format +msgid "Odoo Enterprise" +msgstr "" + +#. module: web_planner +#: model:ir.model,name:web_planner.model_web_planner +#: model:ir.ui.view,arch_db:web_planner.web_planner_view_search +#: model:ir.ui.view,arch_db:web_planner.web_planner_view_tree +msgid "Planner" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_planner_application +msgid "Planner Application" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_tooltip_planner +msgid "Planner Tooltips" +msgstr "" + +#. module: web_planner +#: model:ir.actions.act_window,name:web_planner.web_planner_action +#: model:ir.ui.menu,name:web_planner.web_planner_menu +msgid "Planners" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_progress +msgid "Progress Percentage" +msgstr "" + +#. module: web_planner +#: model:ir.model.fields,field_description:web_planner.field_web_planner_view_id +msgid "Template" +msgstr "" + +#. module: web_planner +#. openerp-web +#: code:addons/web_planner/static/src/js/web_planner_common.js:365 +#, python-format +msgid "Upgrade now" +msgstr "" diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 19d758db641..d05aaba1504 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -4,7 +4,7 @@ import base64 from email.utils import formataddr import re -import uuid +from uuid import uuid4 from odoo import _, api, fields, models, modules, tools from odoo.exceptions import UserError @@ -56,7 +56,7 @@ class Channel(models.Model): ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') - uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False) + uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 44ebafa3242..67e4f5a527b 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime -import uuid +from uuid import uuid4 from odoo import api, fields, models, _ from odoo.exceptions import ValidationError @@ -114,7 +114,7 @@ class PosConfig(models.Model): proxy_ip = fields.Char(string='IP Address', size=45, help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.') active = fields.Boolean(default=True) - uuid = fields.Char(readonly=True, default=lambda self: str(uuid.uuid4()), + uuid = fields.Char(readonly=True, default=lambda self: str(uuid4()), help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.') sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True, help="This sequence is automatically created by Odoo but you can change it " diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 78b4ddc3169..3584f6e5756 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -230,7 +230,7 @@ class ProductProduct(models.Model): def _compute_partner_ref(self): for supplier_info in self.seller_ids: if supplier_info.name.id == self._context.get('partner_id'): - product_name = supplier_info.product_name or self.default_code + product_name = supplier_info.product_name or self.default_code or self.name self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name) break else: diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 789b37b95d3..a4264cd7db8 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -138,8 +138,8 @@ class PurchaseOrder(models.Model): ('invoiced', 'No Bill to Receive'), ], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no') - picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True) - picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True) + picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True, compute_sudo=True) + picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True, compute_sudo=True) # There is no inverse function on purpose since the date may be different on each line date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True) diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index 553c0f2917d..c009d3ba37b 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -186,3 +186,12 @@ class Orderpoint(models.Model): result['domain'] = "[('id','in',%s)]" % (purchase_ids.ids) return result + + +class PushedFlow(models.Model): + _inherit = "stock.location.path" + + def _prepare_move_copy_values(self, move_to_copy, new_date): + res = super()._prepare_move_copy_values(move_to_copy, new_date) + res['purchase_line_id'] = None + return res diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 73d9732dec0..58a100ed06e 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -174,8 +174,8 @@ </button> </div> <div class="oe_title"> - <label string="Request for Quotation " attrs="{'invisible': [('state','not in',('draft','sent','bid'))]}"/> - <label string="Purchase Order " attrs="{'invisible': [('state','in',('draft','sent','bid'))]}"/> + <label string="Request for Quotation " attrs="{'invisible': [('state','not in',('draft','sent'))]}"/> + <label string="Purchase Order " attrs="{'invisible': [('state','in',('draft','sent'))]}"/> <h1> <field name="name" readonly="1"/> </h1> diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 0439e8d7ccb..3f925a755ff 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -262,12 +262,12 @@ class StockQuant(models.Model): # if we want to reserve available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: - raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) + raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % product_id.display_name) elif float_compare(quantity, 0, precision_rounding=rounding) < 0: # if we want to unreserve available_quantity = sum(quants.mapped('reserved_quantity')) if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0: - raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) + raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % product_id.display_name) else: return reserved_quants diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 07916d95fa6..3394c3a1e7b 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -695,11 +695,31 @@ class Warehouse(models.Model): def _get_sequence_values(self): return { - 'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5}, - 'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5}, - 'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5}, - 'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5}, - 'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5}, + 'in_type_id': { + 'name': self.name + ' ' + _('Sequence in'), + 'prefix': self.code + '/IN/', 'padding': 5, + 'company_id': self.company_id.id, + }, + 'out_type_id': { + 'name': self.name + ' ' + _('Sequence out'), + 'prefix': self.code + '/OUT/', 'padding': 5, + 'company_id': self.company_id.id, + }, + 'pack_type_id': { + 'name': self.name + ' ' + _('Sequence packing'), + 'prefix': self.code + '/PACK/', 'padding': 5, + 'company_id': self.company_id.id, + }, + 'pick_type_id': { + 'name': self.name + ' ' + _('Sequence picking'), + 'prefix': self.code + '/PICK/', 'padding': 5, + 'company_id': self.company_id.id, + }, + 'int_type_id': { + 'name': self.name + ' ' + _('Sequence internal'), + 'prefix': self.code + '/INT/', 'padding': 5, + 'company_id': self.company_id.id, + }, } def _format_rulename(self, from_loc, dest_loc, suffix): diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index 5f9263dbbc1..60842b1bef4 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models +from odoo import api, models, fields import logging diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index af58b84a0e7..2cd24d707ae 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -107,7 +107,7 @@ class LandedCost(models.Model): 'landed_cost_value': new_landed_cost_value, 'value': line.move_id.value + cost_to_add, 'remaining_value': line.move_id.remaining_value + cost_to_add, - 'price_unit': (line.move_id.value + new_landed_cost_value) / line.move_id.product_qty, + 'price_unit': (line.move_id.value + cost_to_add) / line.move_id.product_qty, }) # `remaining_qty` is negative if the move is out and delivered proudcts that were not # in stock. diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 933111f6202..f86e5b66f43 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1773,7 +1773,7 @@ var StatInfo = AbstractField.extend({ */ _render: function () { var options = { - value: this.value || 0, + value: this._formatValue(this.value || 0), }; if (! this.attrs.nolabel) { if (this.nodeOptions.label_field && this.recordData[this.nodeOptions.label_field]) { diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 08b7826b4c8..39609758f24 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -4034,6 +4034,40 @@ QUnit.module('basic_fields', { QUnit.module('StatInfo'); + QUnit.test('statinfo widget formats decimal precision', function (assert) { + // sometimes the round method can return numbers such as 14.000001 + // when asked to round a number to 2 decimals, as such is the behaviour of floats. + // we check that even in that eventuality, only two decimals are displayed + assert.expect(2); + + this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'}; + this.data.partner.records[0].monetary = 9.999999; + this.data.partner.records[0].currency_id = 1; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<button class="oe_stat_button" name="items" icon="fa-gear">' + + '<field name="qux" widget="statinfo"/>' + + '</button>' + + '<button class="oe_stat_button" name="money" icon="fa-money">' + + '<field name="monetary" widget="statinfo"/>' + + '</button>' + + '</form>', + res_id: 1, + }); + + // formatFloat renders according to this.field.digits + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(0).text(), + '0.4', "Default precision should be [16,1]"); + assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(1).text(), + '10.00', "Currency decimal precision should be 2"); + + form.destroy(); + }); + QUnit.test('statinfo widget in form view', function (assert) { assert.expect(9); diff --git a/addons/web_editor/static/src/js/editor/translator.js b/addons/web_editor/static/src/js/editor/translator.js index e3ba65a54d7..302f52d2d38 100644 --- a/addons/web_editor/static/src/js/editor/translator.js +++ b/addons/web_editor/static/src/js/editor/translator.js @@ -8,6 +8,7 @@ var Widget = require('web.Widget'); var weContext = require('web_editor.context'); var rte = require('web_editor.rte'); var weWidgets = require('web_editor.widget'); +var Dialog = require('web.Dialog'); var _t = core._t; @@ -26,6 +27,7 @@ var RTETranslatorWidget = rte.Class.extend({ * @override */ _saveElement: function ($el, context, withLang) { + var self = this; if ($el.data('oe-translation-id')) { return this._rpc({ model: 'ir.translation', @@ -35,7 +37,9 @@ var RTETranslatorWidget = rte.Class.extend({ this._getEscapedElement($el).html(), context || weContext.get() ], - }); + }).fail(function (error) { + Dialog.alert(null, error.data.message); + }); } return this._super($el, context, withLang === undefined ? true : withLang); }, diff --git a/doc/reference/security.rst b/doc/reference/security.rst index bacb02c787f..fd6402893c3 100644 --- a/doc/reference/security.rst +++ b/doc/reference/security.rst @@ -71,8 +71,6 @@ This means the first *group rule* restricts access, but any further .. warning:: record rules do not apply to the Administrator user :class: aphorism - although access rules do - .. _reference/security/fields: Field Access diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 4f7eb1e2c25..f0338390889 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -547,23 +547,29 @@ class IrModelFields(models.Model): This method prevents the modification/deletion of many2one fields that have an inverse one2many, for instance. """ + failed_dependencies = [] + for rec in self: + model = self.env[rec.model] + field = model._fields[rec.name] + for dependant, path in model._field_triggers.get(field, ()): + if dependant.manual: + failed_dependencies.append((field, dependant)) + for inverse in model._field_inverses.get(field, ()): + if inverse.manual and inverse.type == 'one2many': + failed_dependencies.append((field, inverse)) + + if not self._context.get(MODULE_UNINSTALL_FLAG) and failed_dependencies: + msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") + raise UserError(msg % failed_dependencies[0]) + elif failed_dependencies: + dependants = {rel[1] for rel in failed_dependencies} + to_unlink = [self._get(field.model_name, field.name) for field in dependants] + self.browse().union(*to_unlink).unlink() + self = self.filtered(lambda record: record.state == 'manual') if not self: return - for record in self: - model = self.env[record.model] - field = model._fields[record.name] - if field.type == 'many2one' and model._field_inverses.get(field): - if self._context.get(MODULE_UNINSTALL_FLAG): - # automatically unlink the corresponding one2many field(s) - inverses = self.search([('relation', '=', field.model_name), - ('relation_field', '=', field.name)]) - inverses.unlink() - continue - msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") - raise UserError(msg % (field, model._field_inverses[field][0])) - # remove fields from registry, and check that views are not broken fields = [self.env[record.model]._pop_field(record.name) for record in self] domain = expression.OR([('arch_db', 'like', record.name)] for record in self) diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 7c5836fa97e..50074648304 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -230,6 +230,7 @@ class TestActionBindings(common.TransactionCase): class TestCustomFields(common.TransactionCase): MODEL = 'res.partner' + COMODEL = 'res.users' def setUp(self): # check that the registry is properly reset @@ -245,9 +246,6 @@ class TestCustomFields(common.TransactionCase): self.registry.enter_test_mode() self.addCleanup(self.registry.leave_test_mode) - # do not reload the registry after removing a field - self.env = self.env(context={'_force_unlink': True}) - def create_field(self, name): """ create a custom field and return it """ model = self.env['ir.model'].search([('model', '=', self.MODEL)]) @@ -332,3 +330,56 @@ class TestCustomFields(common.TransactionCase): with self.assertRaises(UserError): field.name = 'x_bar' self.assertIn('x_foo', self.env[self.MODEL]._fields) + + def test_unlink_with_inverse(self): + """ create a custom o2m and then delete its m2o inverse """ + model = self.env['ir.model']._get(self.MODEL) + comodel = self.env['ir.model']._get(self.COMODEL) + + m2o_field = self.env['ir.model.fields'].create({ + 'model_id': comodel.id, + 'name': 'x_my_m2o', + 'field_description': 'my_m2o', + 'ttype': 'many2one', + 'relation': self.MODEL, + }) + + o2m_field = self.env['ir.model.fields'].create({ + 'model_id': model.id, + 'name': 'x_my_o2m', + 'field_description': 'my_o2m', + 'ttype': 'one2many', + 'relation': self.COMODEL, + 'relation_field': m2o_field.name, + }) + + # normal mode: you cannot break dependencies + with self.assertRaises(UserError): + m2o_field.unlink() + + # uninstall mode: unlink dependant fields + m2o_field.with_context(_force_unlink=True).unlink() + self.assertFalse(o2m_field.exists()) + + def test_unlink_with_dependant(self): + """ create a computed field, then delete its dependency """ + # Also applies to compute fields + comodel = self.env['ir.model'].search([('model', '=', self.COMODEL)]) + + field = self.create_field('x_my_char') + + dependant = self.env['ir.model.fields'].create({ + 'model_id': comodel.id, + 'name': 'x_oh_boy', + 'field_description': 'x_oh_boy', + 'ttype': 'char', + 'related': 'partner_id.x_my_char', + }) + + # normal mode: you cannot break dependencies + with self.assertRaises(UserError): + field.unlink() + + # uninstall mode: unlink dependant fields + field.with_context(_force_unlink=True).unlink() + self.assertFalse(dependant.exists()) diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py index e067c87a5fe..dcea3ba01b1 100644 --- a/odoo/addons/test_pylint/tests/test_pylint.py +++ b/odoo/addons/test_pylint/tests/test_pylint.py @@ -24,9 +24,10 @@ _logger = logging.getLogger(__name__) class TestPyLint(TransactionCase): ENABLED_CODES = [ - 'E0601', # using variable before assignment - 'W0123', # eval used - 'W0101', # unreachable code + 'used-before-assignment', + 'undefined-variable', + 'eval-used', + 'unreachable', 'mixed-indentation', diff --git a/odoo/fields.py b/odoo/fields.py index 3e746e04130..e0ba962d934 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1651,7 +1651,8 @@ class Datetime(Field): # Received data is returned as buffer (in Python 2) or memoryview (in Python 3). _BINARY = memoryview if pycompat.PY2: - _BINARY = buffer #pylint: disable=buffer-builtin + #pylint: disable=buffer-builtin,undefined-variable + _BINARY = buffer class Binary(Field): type = 'binary' diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py index 6e123324635..896f235650b 100644 --- a/odoo/modules/loading.py +++ b/odoo/modules/loading.py @@ -390,7 +390,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") for (model, name) in cr.fetchall(): if model in registry and not registry[model]._abstract and not registry[model]._transient: - _logger.warning('The model %s has no access rules, consider adding one. E.g. access_%s,access_%s,model_%s,,1,0,0,0', + _logger.warning('The model %s has no access rules, consider adding one. E.g. access_%s,access_%s,model_%s,base.group_user,1,0,0,0', model, model.replace('.', '_'), model.replace('.', '_'), model.replace('.', '_')) # Temporary warning while we remove access rights on osv_memory objects, as they have diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index e512d331caa..28a9c1c170d 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -21,7 +21,8 @@ if pycompat.PY2: fp, fname = tools.file_open(path, pathinfo=True) fp2 = None - if not isinstance(fp, file): # pylint: disable=file-builtin + # pylint: disable=file-builtin,undefined-variable + if not isinstance(fp, file): # imp.load_source need a real file object, so we create # one from the file-like object we get from file_open fp2 = os.tmpfile() diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 725971e0143..8b1d7107c0d 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -1139,7 +1139,7 @@ def formatLang(env, value, digits=None, grouping=True, monetary=False, dp=False, digits = decimal_precision_obj.precision_get(dp) elif currency_obj: digits = currency_obj.decimal_places - elif (hasattr(value, '_field') and isinstance(value._field, (float_field, function_field)) and value._field.digits): + elif (hasattr(value, '_field') and getattr(value._field, 'digits', None)): digits = value._field.digits[1] if not digits and digits is not 0: digits = DEFAULT_DIGITS diff --git a/odoo/tools/pycompat.py b/odoo/tools/pycompat.py index 14195c4fa79..5f25e23e977 100644 --- a/odoo/tools/pycompat.py +++ b/odoo/tools/pycompat.py @@ -13,7 +13,7 @@ PY2 = sys.version_info[0] == 2 _Writer = collections.namedtuple('_Writer', 'writerow writerows') if PY2: - # pylint: disable=long-builtin,unichr-builtin,unicode-builtin + # pylint: disable=long-builtin,unichr-builtin,unicode-builtin,undefined-variable unichr = unichr text_type = unicode string_types = (str, unicode) |
