diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-03-05 17:26:04 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-03-05 17:26:04 +0100 |
| commit | 3e54704e669441e655b1fe19ee0fa0646cdbf8d5 (patch) | |
| tree | acddbce8a7f3868c1bf8f882dc959f63134f40ec | |
| parent | 2c495502aa37374f3748d9ff4ab7cf9e8d9a6d6e (diff) | |
| parent | 27081bf6f5f00ccb39f82ae62d14d428bf1c9223 (diff) | |
[MERGE] forward port branch 11.0 up to 27081bf6f5f
50 files changed, 462 insertions, 135 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 5bea59342b2..291be1aa22e 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -644,8 +644,8 @@ class AccountBankStatementLine(models.Model): # Create The payment payment = self.env['account.payment'] + partner_id = self.partner_id or (aml_dict.get('move_line') and aml_dict['move_line'].partner_id) or self.env['res.partner'] if abs(total)>0.00001: - partner_id = self.partner_id and self.partner_id.id or False partner_type = False if partner_id and len(account_types) == 1: partner_type = 'customer' if account_types == receivable_account_type else 'supplier' @@ -660,7 +660,7 @@ class AccountBankStatementLine(models.Model): payment = self.env['account.payment'].create({ 'payment_method_id': payment_methods and payment_methods[0].id or False, 'payment_type': total >0 and 'inbound' or 'outbound', - 'partner_id': self.partner_id and self.partner_id.id or False, + 'partner_id': partner_id.id, 'partner_type': partner_type, 'journal_id': self.statement_id.journal_id.id, 'payment_date': self.date, diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e534ab89eb5..4da13596b88 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -665,12 +665,12 @@ class AccountInvoice(models.Model): msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.') raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) - if type in ('out_invoice', 'out_refund'): - account_id = rec_account.id - payment_term_id = p.property_payment_term_id.id - else: + if type in ('in_invoice', 'in_refund'): account_id = pay_account.id payment_term_id = p.property_supplier_payment_term_id.id + else: + account_id = rec_account.id + payment_term_id = p.property_payment_term_id.id delivery_partner_id = self.get_delivery_partner_id() fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id) diff --git a/addons/account/tests/test_account_invoice_rounding.py b/addons/account/tests/test_account_invoice_rounding.py index f0a7774d61d..38c7193a2f1 100644 --- a/addons/account/tests/test_account_invoice_rounding.py +++ b/addons/account/tests/test_account_invoice_rounding.py @@ -3,6 +3,7 @@ from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.exceptions import ValidationError from odoo.tests import tagged +from odoo.tools import mute_logger import time @@ -60,7 +61,8 @@ class TestAccountInvoiceRounding(AccountingTestCase): # to avoid a ValidationError from _check_cash_rounding because the onchange # are not well triggered in the tests. try: - invoice_id.cash_rounding_id = cash_rounding_id + with mute_logger('odoo.models'): + invoice_id.cash_rounding_id = cash_rounding_id except ValidationError: pass diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 03df5bd89ce..9f1190b934c 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -8,6 +8,7 @@ from collections import defaultdict from datetime import datetime, timedelta from odoo import api, exceptions, fields, models, _ +from odoo.tools import pycompat class SignupError(Exception): pass @@ -28,16 +29,16 @@ class ResPartner(models.Model): signup_token = fields.Char(copy=False, groups="base.group_erp_manager") signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager") signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager") - signup_valid = fields.Boolean(compute='_compute_signup_valid', compute_sudo=True, string='Signup Token is Valid') + signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') @api.multi @api.depends('signup_token', 'signup_expiration') def _compute_signup_valid(self): dt = now() - for partner in self: - partner.signup_valid = bool(partner.signup_token) and \ - (not partner.signup_expiration or dt <= partner.signup_expiration) + for partner, partner_sudo in pycompat.izip(self, self.sudo()): + partner.signup_valid = bool(partner_sudo.signup_token) and \ + (not partner_sudo.signup_expiration or dt <= partner_sudo.signup_expiration) @api.multi def _compute_signup_url(self): diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 2ac4deaed5f..2834dfb11c9 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -64,3 +64,17 @@ class SaleOrderLine(models.Model): @api.onchange('event_ticket_id') def _onchange_event_ticket_id(self): self.price_unit = (self.event_id.company_id or self.env.user.company_id).currency_id.compute(self.event_ticket_id.price, self.order_id.currency_id) + + @api.onchange('product_uom', 'product_uom_qty') + def product_uom_change(self): + if not self.event_ticket_id: + super(SaleOrderLine, self).product_uom_change() + + @api.multi + @api.onchange('product_id') + def product_id_change(self): + super(SaleOrderLine, self).product_id_change() + self.event_id = None + self.event_ticket_id = None + if self.product_id.event_ok: + self.price_unit = 0.0 diff --git a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py index 5bbb6848bd3..4c66db60db9 100644 --- a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py +++ b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py @@ -10,7 +10,7 @@ class GamificationBadgeUserWizard(models.TransientModel): employee_id = fields.Many2one('hr.employee', string='Employee', required=True) user_id = fields.Many2one('res.users', string='User', - related='employee_id.user_id', store=True, readonly=True) + related='employee_id.user_id', store=True, readonly=True, compute_sudo=True) # TODO 12.0/master remove this hack by changing the model @api.model diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 35286c1fc9d..2a9fb451f33 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -92,7 +92,7 @@ class HolidaysRequest(models.Model): payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the leave has been taken into account in the payslip.') report_note = fields.Text('HR Comments') - user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) + user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, required=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') date_to = fields.Datetime('End Date', readonly=True, copy=False, required=True, diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 032baf2d971..88284475764 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -77,6 +77,27 @@ class AccountFrFec(models.TransientModel): listrow = list(row) return listrow + def _get_company_legal_data(self, company): + """ + Dom-Tom are excluded from the EU's fiscal territory + Those regions do not have SIREN + sources: + https://www.service-public.fr/professionnels-entreprises/vosdroits/F23570 + http://www.douane.gouv.fr/articles/a11024-tva-dans-les-dom + """ + dom_tom_group = self.env.ref('l10n_fr.dom-tom') + is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped('code') + if not is_dom_tom and not company.vat: + raise Warning( + _("Missing VAT number for company %s") % company.name) + if not is_dom_tom and company.vat[0:2] != 'FR': + raise Warning( + _("FEC is for French companies only !")) + + return { + 'siren': company.vat[4:13] if not is_dom_tom else '', + } + @api.multi def generate_fec(self): self.ensure_one() @@ -88,6 +109,9 @@ class AccountFrFec(models.TransientModel): # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration + company = self.env.user.company_id + company_legal_data = self._get_company_legal_data(company) + header = [ u'JournalCode', # 0 u'JournalLib', # 1 @@ -109,18 +133,7 @@ class AccountFrFec(models.TransientModel): u'Idevise', # 17 ] - company = self.env.user.company_id - if not company.vat: - raise Warning( - _("Missing VAT number for company %s") % company.name) - if company.vat[0:2] != 'FR': - raise Warning( - _("FEC is for French companies only !")) - - fecfile = io.BytesIO() - w = pycompat.csv_writer(fecfile, delimiter='|') - w.writerow(header) - + rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref('account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once @@ -196,7 +209,8 @@ class AccountFrFec(models.TransientModel): else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') - w.writerow(listrow) + rows_to_write.append(listrow) + #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results @@ -207,7 +221,7 @@ class AccountFrFec(models.TransientModel): if unaffected_earnings_account: unaffected_earnings_results[4] = unaffected_earnings_account.code unaffected_earnings_results[5] = unaffected_earnings_account.name - w.writerow(unaffected_earnings_results) + rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' @@ -265,7 +279,7 @@ class AccountFrFec(models.TransientModel): for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() - w.writerow(listrow) + rows_to_write.append(listrow) # LINES sql_query = ''' @@ -330,20 +344,19 @@ class AccountFrFec(models.TransientModel): sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): - w.writerow(list(row)) + rows_to_write.append(list(row)) - siren = company.vat[4:13] + fecvalue = self._csv_write_rows(rows_to_write) end_date = self.date_to.replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' - fecvalue = fecfile.getvalue() + self.write({ 'fec_data': base64.encodestring(fecvalue), # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date - 'filename': '%sFEC%s%s.csv' % (siren, end_date, suffix), + 'filename': '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix), }) - fecfile.close() action = { 'name': 'FEC', @@ -352,3 +365,29 @@ class AccountFrFec(models.TransientModel): 'target': 'self', } return action + + def _csv_write_rows(self, rows, lineterminator=u'\r\n'): + """ + Write FEC rows into a file + It seems that Bercy's bureaucracy is not too happy about the + empty new line at the End Of File. + + @param {list(list)} rows: the list of rows. Each row is a list of strings + @param {unicode string} [optional] lineterminator: effective line terminator + Has nothing to do with the csv writer parameter + The last line written won't be terminated with it + + @return the value of the file + """ + fecfile = io.BytesIO() + writer = pycompat.csv_writer(fecfile, delimiter='|', lineterminator='') + + rows_length = len(rows) + for i, row in enumerate(rows): + if not i == rows_length - 1: + row.append(lineterminator) + writer.writerow(row) + + fecvalue = fecfile.getvalue() + fecfile.close() + return fecvalue diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 33c4b76d79d..36ced25ed75 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -84,8 +84,8 @@ class MailActivity(models.Model): activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity', domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict') - activity_category = fields.Selection(related='activity_type_id.category') - icon = fields.Char('Icon', related='activity_type_id.icon') + activity_category = fields.Selection(related='activity_type_id.category', readonly=True) + icon = fields.Char('Icon', related='activity_type_id.icon', readonly=True) summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') @@ -103,8 +103,8 @@ class MailActivity(models.Model): ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') - recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type") - previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type') + recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True) + previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True) has_recommended_activities = fields.Boolean( 'Next activities available', compute='_compute_has_recommended_activities', diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index ec28fb6fa15..ecbfedee859 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1477,6 +1477,8 @@ td { body { margin: 0; color: white !important; + /* avoid black background if backgrounds are printed */ + background: initial; } .oe_leftbar, .oe_loading, diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index 44eb1637619..9fa0e12561e 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -35,7 +35,7 @@ class PortalWizard(models.TransientModel): contact_ids = set() user_changes = [] for partner in self.env['res.partner'].sudo().browse(partner_ids): - contact_partners = partner.child_ids or [partner] + contact_partners = partner.child_ids | partner for contact in contact_partners: # make sure that each contact appears at most once in the list if contact.id not in contact_ids: diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index c0d1b404707..4f625948dfc 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -241,7 +241,11 @@ class Pricelist(models.Model): break # Final price conversion into pricelist currency if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist': - price = product.currency_id.compute(price, self.currency_id, round=False) + if suitable_rule.base == 'standard_price': + # The cost of the product is always in the company currency + price = product.cost_currency_id.compute(price, self.currency_id, round=False) + else: + price = product.currency_id.compute(price, self.currency_id, round=False) results[product.id] = (price, suitable_rule and suitable_rule.id or False) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 766aadefb37..8a31a68fc8b 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -62,6 +62,8 @@ class ProductTemplate(models.Model): currency_id = fields.Many2one( 'res.currency', 'Currency', compute='_compute_currency_id') + cost_currency_id = fields.Many2one( + 'res.currency', 'Cost Currency', compute='_compute_cost_currency_id') # price fields price = fields.Float( @@ -165,6 +167,10 @@ class ProductTemplate(models.Model): for template in self: template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id + def _compute_cost_currency_id(self): + for template in self: + template.cost_currency_id = self.env.user.company_id.currency_id.id + @api.multi def _compute_template_price(self): prices = {} diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index a47a49a87b4..1da4cdcc99e 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -51,13 +51,14 @@ <field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/> <label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"/> <div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"> - <field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/> + <field name="standard_price" widget='monetary' options="{'currency_field': 'cost_currency_id'}"/> </div> <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> <field name="uom_id" groups="uom.group_uom" options="{'no_create': True}"/> <field name="uom_po_id" groups="uom.group_uom" options="{'no_create': True}"/> <field name="currency_id" invisible="1"/> + <field name="cost_currency_id" invisible="1"/> <field name="product_variant_id" invisible="1"/> </group> <group name="group_lots_and_weight" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"> diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py index d03c877b938..a1f60defb2d 100644 --- a/addons/sale/models/res_partner.py +++ b/addons/sale/models/res_partner.py @@ -14,14 +14,20 @@ class ResPartner(models.Model): sale_warn_msg = fields.Text('Message for Sales Order') def _compute_sale_order_count(self): - sale_data = self.env['sale.order'].read_group(domain=[('partner_id', 'child_of', self.ids)], - fields=['partner_id'], groupby=['partner_id']) - mapped_data = dict([(m['partner_id'][0], m['partner_id_count']) for m in sale_data]) - for partner in self: - # let's obtain the partner id and all its child ids - partner_ids = [partner.id] + partner.child_ids.ids - # then we can sum for all the partner's child - partner.sale_order_count = sum(mapped_data.get(child, 0) for child in partner_ids) + # retrieve all children partners and prefetch 'parent_id' on them + all_partners = self.search([('id', 'child_of', self.ids)]) + all_partners.read(['parent_id']) + + sale_order_groups = self.env['sale.order'].read_group( + domain=[('partner_id', 'in', all_partners.ids)], + fields=['partner_id'], groupby=['partner_id'] + ) + for group in sale_order_groups: + partner = self.browse(group['partner_id'][0]) + while partner: + if partner in self: + partner.sale_order_count += group['partner_id_count'] + partner = partner.parent_id def can_edit_vat(self): can_edit_vat = super(ResPartner, self).can_edit_vat() diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 1127059bc9a..e108a77a61e 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1151,7 +1151,8 @@ class StockMove(models.Model): moves_todo.mapped('move_dest_ids')._action_assign() # We don't want to create back order for scrap moves - if all(move_todo.scrapped for move_todo in moves_todo): + # Replace by a kwarg in master + if self.env.context.get('is_scrap'): return moves_todo if picking: diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 5481939602f..7f173c8e511 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -703,7 +703,7 @@ class Picking(models.Model): # If no lots when needed, raise error picking_type = self.picking_type_id precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') - no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids) + no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))) no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) if no_reserved_quantities and no_quantities_done: raise UserError(_('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.')) diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index 31fbbabec84..5f437b611f5 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -105,7 +105,8 @@ class StockScrap(models.Model): def do_scrap(self): for scrap in self: move = self.env['stock.move'].create(scrap._prepare_move_values()) - move._action_done() + # master: replace context by cancel_backorder + move.with_context(is_scrap=True)._action_done() scrap.write({'move_id': move.id, 'state': 'done'}) return True diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index dd12cbf8a22..1d040e8d91a 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -3383,6 +3383,15 @@ class StockMove(TransactionCase): }) picking.action_confirm() picking.action_assign() + + scrap = self.env['stock.scrap'].create({ + 'picking_id': picking.id, + 'product_id': self.product1.id, + 'product_uom_id': self.uom_unit.id, + 'scrap_qty': 5.0, + }) + scrap.do_scrap() + # No products are reserved on the move of 10, click on `button_validate`. with self.assertRaises(UserError): picking.button_validate() diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml index 3573835625e..943c6dd34d5 100644 --- a/addons/stock_account/views/product_views.xml +++ b/addons/stock_account/views/product_views.xml @@ -40,7 +40,7 @@ <field name="standard_price" position="replace"> <field name="standard_price" attrs="{'readonly':[('valuation','=','real_time')]}" - widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" + widget='monetary' options="{'currency_field': 'cost_currency_id', 'field_digits': True}" class="pull-left"/> <div name="update_cost_price" class="col-md-2"> <button string="Update Cost" type="action" diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index 4a38664ec38..14d04f0ec39 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -77,7 +77,8 @@ <field name="qty_at_date"/> <field name="uom_id" groups="uom.group_uom"/> <field name="currency_id" invisible="1"/> - <field name="stock_value" sum="Stock Valuation" widget="monetary"/> + <field name="cost_currency_id" invisible="1"/> + <field name="stock_value" sum="Stock Valuation" widget="monetary" options="{'currency_field': 'cost_currency_id'}"/> <field name="cost_method" invisible="1"/> <button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" /> </tree> diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 6dbf96baba3..4c8fae324b0 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -194,7 +194,7 @@ var FieldMany2One = AbstractField.extend({ _bindAutoComplete: function () { var self = this; // avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439 - if (this.$input.attr('placeholder')) { + if ($.browser.chrome && this.$input.attr('placeholder')) { this.$input.attr('placeholder', function (index, val) { return val.split('').join('\ufeff'); }); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 4978e6ae920..1f00b726630 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -623,14 +623,14 @@ var PivotModel = AbstractModel.extend({ }); var index = 0; - var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys; + var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys.slice(); + this.data.groupedBy = rowGroupBys; var colGroupBys = this.data.colGroupBys; var datapt, row, col, attrs, cell_value; var main_row_header, main_col_header; var groupBys; var m; - for (var i = 0; i < rowGroupBys.length + 1; i++) { for (var j = 0; j < colGroupBys.length + 1; j++) { for (var k = 0; k < data[index].length; k++) { @@ -757,8 +757,8 @@ var PivotModel = AbstractModel.extend({ */ _updateMainGroupBys: function (old, main) { var new_groupby_length = this._getHeaderDepth(main.root) - 1; - var new_groupby_list = old.root.groupbys.slice(0, new_groupby_length); - main.root.groupbys = new_groupby_list; + main.root.groupbys = old.root.groupbys; + main.root.groupbys.splice(new_groupby_length); }, /** * @param {Object} old_tree diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index c32435a9b43..41b4698ccd4 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -994,6 +994,48 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('Reload, group by columns, reload', function (assert) { + assert.expect(2); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot/>', + }); + + // Set a column groupby + pivot.$('thead .o_pivot_header_cell_closed').click(); + pivot.$('.o_field_selection li[data-field=customer] a').click(); + + // Set a domain + pivot.update({domain: [['product_id', '=', 41]]}); + + var expectedContext = {pivot_column_groupby: ['customer'], + pivot_measures: ['__count'], + pivot_row_groupby: []}; + + // Check that column groupbys were not lost + assert.deepEqual(pivot.getContext(), expectedContext, + 'Column groupby not lost after first reload'); + + // Set a column groupby + pivot.$('thead .o_pivot_header_cell_closed').click(); + pivot.$('.o_field_selection li[data-field=product_id] a').click(); + + // Set a domain + pivot.update({domain: [['product_id', '=', 37]]}); + + var expectedContext = {pivot_column_groupby: ['customer', 'product_id'], + pivot_measures: ['__count'], + pivot_row_groupby: []}; + + assert.deepEqual(pivot.getContext(), expectedContext, + 'Column groupby not lost after second reload'); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index 37228224971..cbb7e3ef643 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -189,5 +189,6 @@ class IrUiView(models.Model): ``bundles=True`` returns also the asset bundles """ user_groups = set(self.env.user.groups_id) - views = self.with_context(active_test=False)._views_get(key, bundles=bundles) + View = self.with_context(active_test=False, lang=None) + views = View._views_get(key, bundles=bundles) return views.filtered(lambda v: not v.groups_id or len(user_groups.intersection(v.groups_id))) diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 6599000d245..e731ec0fd99 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -84,6 +84,16 @@ var SnippetEditor = Widget.extend({ }); } + this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', function () { + self._targetIsAnimated = true; + }); + this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', function () { + self._targetIsAnimated = false; + if (self.$el.is('.oe_active')) { + self.cover(); + } + }); + return $.when.apply($, defs); }, /** @@ -92,6 +102,7 @@ var SnippetEditor = Widget.extend({ destroy: function () { this._super.apply(this, arguments); this.$target.removeData('snippet-editor'); + this.$target.off('.snippet_editor'); }, //-------------------------------------------------------------------------- @@ -123,6 +134,11 @@ var SnippetEditor = Widget.extend({ * Makes the editor overlay cover the associated snippet. */ cover: function () { + if (this._targetIsAnimated) { + // Do not cover a target being animated, it will be covered once the + // animation is completed. + return; + } var offset = this.$target.offset(); var manipulatorOffset = this.$el.parent().offset(); offset.top -= manipulatorOffset.top; diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 49285986240..e7580a4aa3b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -289,13 +289,11 @@ var SnippetOption = Widget.extend({ .addClass('active'); _processSelectClassElements(this.$el); - _.each(this.$el.find('.dropdown-menu'), function (group) { - _processSelectClassElements($(group).children()); - }); - function _processSelectClassElements($elements) { + function _processSelectClassElements($el) { var maxNbClasses = -1; - $elements.filter('[data-select-class]') + $el.find('[data-select-class]') + .addBack('[data-select-class]') .removeClass('active') .filter(function () { var className = $(this).data('selectClass'); diff --git a/addons/website/__init__.py b/addons/website/__init__.py index c158f87e14f..dcd5ec789d0 100644 --- a/addons/website/__init__.py +++ b/addons/website/__init__.py @@ -4,3 +4,19 @@ from . import controllers from . import models from . import wizard + +import odoo +from odoo import api, SUPERUSER_ID +from functools import partial + + +def uninstall_hook(cr, registry): + def rem_website_id_null(dbname): + db_registry = odoo.modules.registry.Registry.new(dbname) + with api.Environment.manage(), db_registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + env['ir.model.fields'].search([ + ('name', '=', 'website_id'), + ('model', '=', 'res.config.settings'), + ]).unlink() + cr.after('commit', partial(rem_website_id_null, cr.dbname)) diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index fbedd44425b..3a9264a0624 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -34,4 +34,5 @@ ], 'qweb': ['static/src/xml/website.backend.xml'], 'application': True, + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 750744ce7d1..e9e790cc0ca 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -11,4 +11,3 @@ from . import res_company from . import res_partner from . import res_config_settings from . import ir_model_fields -from . import ir_model diff --git a/addons/website/models/ir_model.py b/addons/website/models/ir_model.py deleted file mode 100644 index 84c6a0b1836..00000000000 --- a/addons/website/models/ir_model.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details - -from odoo import api, models - - -# This is a nasty hack, targeted for V11 only -class IrModel(models.Model): - _inherit = 'ir.model' - - @api.multi - def unlink(self): - self.env.cr.execute("DELETE FROM ir_model_fields WHERE name='website_id'") - self.env.cr.execute("DELETE FROM res_config_settings WHERE website_id IS NOT NULL") - return super(IrModel, self).unlink() diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js index 47402c016c8..843cc4898f2 100644 --- a/addons/website/static/src/js/tours/rte.js +++ b/addons/website/static/src/js/tours/rte.js @@ -150,5 +150,25 @@ tour.register('rte_translator', { }, { content: "check bis: placeholder translation", trigger: 'input[placeholder="test french placeholder"]', +}, { + content: "Open customize menu", + trigger: "#customize-menu > .dropdown-toggle", +}, { + content: "Open HTML editor", + trigger: "[data-action='ace']", +}, { + content: "Check that the editor is not showing translated content (1)", + trigger: '.ace_text-layer .ace_line:contains("an HTML")', + run: function (actions) { + var lineEscapedText = $(this.$anchor.text()).text(); + if (lineEscapedText !== "<b></b> is an HTML tag & is empty") { + console.error('The HTML editor should display the correct untranslated content'); + $('body').addClass('rte_translator_error'); + } + }, +}, { + content: "Check that the editor is not showing translated content (2)", + trigger: 'body:not(.rte_translator_error)', + run: function () {}, }]); }); diff --git a/addons/website_event_sale/models/__init__.py b/addons/website_event_sale/models/__init__.py index 1bd7829ab86..a9c94cbe9ae 100644 --- a/addons/website_event_sale/models/__init__.py +++ b/addons/website_event_sale/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from . import product +from . import product_pricelist from . import sale_order from . import website diff --git a/addons/website_event_sale/models/product_pricelist.py b/addons/website_event_sale/models/product_pricelist.py new file mode 100644 index 00000000000..5b0d4465759 --- /dev/null +++ b/addons/website_event_sale/models/product_pricelist.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from odoo import _, api, models + +class PricelistItem(models.Model): + _inherit = "product.pricelist.item" + + @api.onchange('applied_on', 'product_id', 'product_tmpl_id', 'min_quantity') + def _onchange_event_sale_warning(self): + if self.min_quantity > 0: + msg = '' + if self.applied_on == '3_global' or self.applied_on == '2_product_category': + msg = _("A pricelist item with a positive min. quantity will not be applied to the event tickets products.") + elif ((self.applied_on == '1_product' and self.product_tmpl_id.event_ok) or + (self.applied_on == '0_product_variant' and self.product_id.event_ok)): + msg = _("A pricelist item with a positive min. quantity cannot be applied to this event tickets product.") + if msg: + return {'warning': + { + 'title': _("Warning"), + 'message': msg + } + } diff --git a/addons/website_quote/models/sale_quote.py b/addons/website_quote/models/sale_quote.py index 820c9ac8f24..9b32e69944b 100644 --- a/addons/website_quote/models/sale_quote.py +++ b/addons/website_quote/models/sale_quote.py @@ -117,7 +117,10 @@ class SaleQuoteOption(models.Model): product = self.product_id self.price_unit = product.list_price self.website_description = product.product_tmpl_id.quote_description - self.name = product.name + name = product.name + if self.product_id.description_sale: + name += '\n' + self.product_id.description_sale + self.name = name self.uom_id = product.uom_id domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]} return {'domain': domain} diff --git a/doc/cla/corporate/adhoc.md b/doc/cla/corporate/adhoc.md index 428f46b0056..ca781cae7a5 100644 --- a/doc/cla/corporate/adhoc.md +++ b/doc/cla/corporate/adhoc.md @@ -14,3 +14,4 @@ Damián Soriano ds@adhoc.com.ar https://github.com/damiansoriano Juan José Scarafía jjs@adhoc.com.ar https://github.com/jjscarafia Nicolás Mac Rouillon nmr@adhoc.com.ar https://github.com/nicomacr Katherine Zaoral kz@adhoc.com.ar https://github.com/zaoral +Valentino Defelice vd@adhoc.com.ar https://github.com/ValentinoDefelice diff --git a/doc/cla/corporate/projet-grup.md b/doc/cla/corporate/projet-grup.md deleted file mode 100644 index 635f8d5148c..00000000000 --- a/doc/cla/corporate/projet-grup.md +++ /dev/null @@ -1,18 +0,0 @@ -Turkey, 2017-12-05 - -Projet Yazilim Ve Danismanlik AS agrees to the terms of the Odoo Corporate Contributor License -Agreement v1.0. - -I declare that I am authorized and able to make this agreement and sign this -declaration. - -Signed, - -Can Tecim can.tecim@gmail.com https://github.com/cantecim - -List of contributors: - -Can Tecim can.tecim@gmail.com https://github.com/cantecim -Can Tecim dev@projetgrup.com https://github.com/devprojetgrup -Selahattin Ünlü selahattin.unlu@yandex.com https://github.com/selahattinunlu -Abdurrahman Işık isikabdurrahman@yahoo.com https://github.com/abdurrahman diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md index 6dca085c4ce..913e317cb07 100644 --- a/doc/cla/corporate/savoirfairelinux.md +++ b/doc/cla/corporate/savoirfairelinux.md @@ -21,6 +21,8 @@ Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://githu Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb Rim Ben Dhaou rim.bendhaou@savoirfairelinux.com https://github.com/rimbendhaou Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini +Luis Garcia Ruiz luis.garcia@savoirfairelinux.com https://github.com/eilst +Dimitar Apostolov dimitar.apostolov@savoirfairelinux.com https://github.com/dapostolov-sfl Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard (up to 2017-10-17) Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL (up to 2017-10-17) Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche (up to 2017-10-17) diff --git a/doc/cla/individual/cantecim.md b/doc/cla/individual/cantecim.md new file mode 100644 index 00000000000..1aa35478772 --- /dev/null +++ b/doc/cla/individual/cantecim.md @@ -0,0 +1,11 @@ +Turkey, 2019-02-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Can Tecim can.tecim@gmail.com https://github.com/cantecim diff --git a/doc/cla/individual/mjabuz.md b/doc/cla/individual/mjabuz.md new file mode 100644 index 00000000000..55e9589f992 --- /dev/null +++ b/doc/cla/individual/mjabuz.md @@ -0,0 +1,11 @@ +Bangladesh, 16 February, 2019 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Abu Zafar azmikbal@gmail.com https://github.com/MjAbuz diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index c14f5d35a32..ef66fc97181 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -181,6 +181,9 @@ class IrModel(models.Model): # prevent screwing up fields that depend on these models' fields model.field_id._prepare_update() + # delete fields whose comodel is being removed + self.env['ir.model.fields'].search([('relation', 'in', self.mapped('model'))]).unlink() + self._drop_table() res = super(IrModel, self).unlink() diff --git a/odoo/addons/base/models/ir_rule.py b/odoo/addons/base/models/ir_rule.py index 13068389cce..a319b74a1a2 100644 --- a/odoo/addons/base/models/ir_rule.py +++ b/odoo/addons/base/models/ir_rule.py @@ -96,6 +96,8 @@ class IrRule(models.Model): group_domains.append(dom) # combine global domains and group domains + if not group_domains: + return expression.AND(global_domains) return expression.AND(global_domains + [expression.OR(group_domains)]) @api.model diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py index f2df8865e9e..6b67ee1a9f3 100644 --- a/odoo/addons/base/models/qweb.py +++ b/odoo/addons/base/models/qweb.py @@ -31,6 +31,8 @@ try: except ImportError: astor = None +from odoo.tools.parse_version import parse_version + unsafe_eval = eval _logger = logging.getLogger(__name__) @@ -176,7 +178,7 @@ class QWebException(Exception): return str(self) # Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9 -escape = (lambda text: _escape(text, quote=True)) if getattr(werkzeug, '__version__', '0.0') < '0.9.0' else _escape +escape = (lambda text: _escape(text, quote=True)) if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0') else _escape def foreach_iterator(base_ctx, enum, name): ctx = base_ctx.copy() @@ -1457,7 +1459,7 @@ class QWeb(object): ) if call_options: - # update this dict with the content of `t-call-options` + # update this dict with the content of `t-call-options` content.extend([ # options_.update(template options) ast.Expr(ast.Call( @@ -1468,7 +1470,56 @@ class QWeb(object): ), args=[self._compile_expr(call_options)], keywords=[], starargs=None, kwargs=None - )) + )), + + # if options.get('lang') != options_.get('lang'): + # self = self.with_context(lang=options.get('lang')) + ast.If( + test=ast.Compare( + left=ast.Call( + func=ast.Attribute( + value=ast.Name(id='options', ctx=ast.Load()), + attr='get', + ctx=ast.Load() + ), + args=[ast.Str("lang")], keywords=[], + starargs=None, kwargs=None + ), + ops=[ast.NotEq()], + comparators=[ast.Call( + func=ast.Attribute( + value=ast.Name(id=name_options, ctx=ast.Load()), + attr='get', + ctx=ast.Load() + ), + args=[ast.Str("lang")], keywords=[], + starargs=None, kwargs=None + )] + ), + body=[ + ast.Assign( + targets=[ast.Name(id='self', ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='with_context', + ctx=ast.Load() + ), + args=[], + keywords=[ast.keyword('lang', ast.Call( + func=ast.Attribute( + value=ast.Name(id=name_options, ctx=ast.Load()), + attr='get', + ctx=ast.Load() + ), + args=[ast.Str("lang")], keywords=[], + starargs=None, kwargs=None + ))], + starargs=None, kwargs=None + ) + )], + orelse=[], + ) ]) if nsmap: diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 4949ce9cb42..96c6b221a40 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -422,20 +422,19 @@ class Users(models.Model): @tools.ormcache('self._uid') def context_get(self): user = self.env.user - result = {} - for k in self._fields: - if k.startswith('context_'): - context_key = k[8:] - elif k in ['lang', 'tz']: - context_key = k - else: - context_key = False - if context_key: - res = getattr(user, k) or False - if isinstance(res, models.BaseModel): - res = res.id - result[context_key] = res or False - return result + # determine field names to read + name_to_key = { + name: name[8:] if name.startswith('context_') else name + for name in self._fields + if name.startswith('context_') or name in ('lang', 'tz') + } + # use read() to not read other fields: this must work while modifying + # the schema of models res.users or res.partner + values = user.read(list(name_to_key), load=False)[0] + return { + key: values[name] + for name, key in name_to_key.items() + } @api.model @api.returns('ir.actions.act_window', lambda record: record.id) diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 969c3f8eb6c..2554cfed135 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -640,6 +640,36 @@ class TestExpression(TransactionCase): '&', ('foo', '=', 2), ('bar', '=', 2)] self.assertEqual(expression.OR([d1, d2]), expected) + def test_proper_combine_unit_leaves(self): + # test that unit leaves (TRUE_LEAF, FALSE_LEAF) are properly handled in specific cases + false = expression.FALSE_DOMAIN + true = expression.TRUE_DOMAIN + normal = [('foo', '=', 'bar')] + # OR with single FALSE_LEAF + expr = expression.OR([false]) + self.assertEqual(expr, false) + # OR with multiple FALSE_LEAF + expr = expression.OR([false, false]) + self.assertEqual(expr, false) + # OR with FALSE_LEAF and a normal leaf + expr = expression.OR([false, normal]) + self.assertEqual(expr, normal) + # OR with AND of single TRUE_LEAF and normal leaf + expr = expression.OR([expression.AND([true]), normal]) + self.assertEqual(expr, true) + # AND with single TRUE_LEAF + expr = expression.AND([true]) + self.assertEqual(expr, true) + # AND with multiple TRUE_LEAF + expr = expression.AND([true, true]) + self.assertEqual(expr, true) + # AND with TRUE_LEAF and normal leaves + expr = expression.AND([true, normal]) + self.assertEqual(expr, normal) + # AND with OR with single FALSE_LEAF and normal leaf + expr = expression.AND([expression.OR([false]), normal]) + self.assertEqual(expr, false) + class TestAutoJoin(TransactionCase): diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py index 3b0f6d768cc..db688634185 100644 --- a/odoo/addons/base/tests/test_qweb.py +++ b/odoo/addons/base/tests/test_qweb.py @@ -489,6 +489,47 @@ class TestQWebNS(TransactionCase): with self.assertRaises(QWebException, msg=error_msg): view1.render() + def test_render_t_call_propagates_t_lang(self): + current_lang = 'en_US' + other_lang = 'fr_FR' + + self.env['res.lang'].load_lang(lang=other_lang) + + self.env['res.lang'].search([('code', '=', other_lang)], limit=1).write({ + 'active': True, + 'decimal_point': '*', + 'thousands_sep': '/' + }) + + view1 = self.env['ir.ui.view'].create({ + 'name': "callee", + 'type': 'qweb', + 'arch': u""" + <t t-name="base.callee"> + <t t-esc="9000000.00" t-options="{'widget': 'float', 'precision': 2}" /> + </t> + """ + }) + self.env['ir.model.data'].create({ + 'name': 'callee', + 'model': 'ir.ui.view', + 'module': 'base', + 'res_id': view1.id, + }) + + view2 = self.env['ir.ui.view'].create({ + 'name': "calling", + 'type': 'qweb', + 'arch': u""" + <t t-name="base.calling"> + <t t-call="base.callee" t-lang="'%s'" /> + </t> + """ % other_lang + }) + + rendered = view2.with_context(lang=current_lang).render().strip() + self.assertEqual(rendered, b'9/000/000*00') + from copy import deepcopy class FileSystemLoader(object): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index d9cb3034917..7266e90d9d1 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -866,7 +866,7 @@ class TestViews(ViewCase): # implemeted elsewhere... modifiers_tests() - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_invalid_field(self): with self.assertRaises(ValidationError): self.View.create({ @@ -880,7 +880,7 @@ class TestViews(ViewCase): """, }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_invalid_subfield(self): with self.assertRaises(ValidationError): self.View.create({ @@ -899,7 +899,7 @@ class TestViews(ViewCase): """, }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_context_in_view(self): arch = """ <form string="View"> @@ -919,7 +919,7 @@ class TestViews(ViewCase): 'arch': arch % '', }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_context_in_subview(self): arch = """ <form string="View"> @@ -951,7 +951,7 @@ class TestViews(ViewCase): 'arch': arch % ('<field name="model"/>', ''), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_context_in_subview_with_parent(self): arch = """ <form string="View"> @@ -982,7 +982,7 @@ class TestViews(ViewCase): 'arch': arch % ('', '<field name="model"/>'), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_context_in_subsubview_with_parent(self): arch = """ <form string="View"> @@ -1024,7 +1024,7 @@ class TestViews(ViewCase): 'arch': arch % ('', '', '<field name="model"/>'), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_domain_in_view(self): arch = """ <form string="View"> @@ -1044,7 +1044,7 @@ class TestViews(ViewCase): 'arch': arch % '', }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_domain_in_subview(self): arch = """ <form string="View"> @@ -1075,7 +1075,7 @@ class TestViews(ViewCase): 'arch': arch % ('<field name="model"/>', ''), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_domain_in_subview_with_parent(self): arch = """ <form string="View"> @@ -1106,7 +1106,7 @@ class TestViews(ViewCase): 'arch': arch % ('', '<field name="model"/>'), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_domain_on_field_in_view(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', model)]") @@ -1129,7 +1129,7 @@ class TestViews(ViewCase): 'arch': arch % '', }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_domain_on_field_in_subview(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', model)]") @@ -1163,7 +1163,7 @@ class TestViews(ViewCase): 'arch': arch % ('<field name="model"/>', ''), }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_domain_on_field_in_subview_with_parent(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', parent.model)]") @@ -1197,7 +1197,7 @@ class TestViews(ViewCase): 'arch': arch % ('', '<field name="model"/>'), }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_domain_on_field_in_noneditable_subview(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', model)]") @@ -1225,7 +1225,7 @@ class TestViews(ViewCase): 'arch': arch % ' editable="bottom"', }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_domain_on_readonly_field_in_view(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', model)]") @@ -1255,7 +1255,7 @@ class TestViews(ViewCase): 'arch': arch, }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_domain_on_readonly_field_in_subview(self): field = self.env['ir.ui.view']._fields['inherit_id'] self.patch(field, 'domain', "[('model', '=', model)]") @@ -1283,7 +1283,7 @@ class TestViews(ViewCase): 'arch': arch % '', }) - @mute_logger('odoo.addons.base.ir.ir_ui_view') + @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models') def test_attrs_field(self): arch = """ <form string="View"> @@ -1304,7 +1304,7 @@ class TestViews(ViewCase): 'arch': arch % '', }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_attrs_subfield(self): arch = """ <form string="View"> @@ -1336,7 +1336,7 @@ class TestViews(ViewCase): 'arch': arch % ('<field name="model"/>', ''), }) - @mute_logger('odoo.addons.base.models.ir_ui_view') + @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models') def test_attrs_subfield_with_parent(self): arch = """ <form string="View"> diff --git a/odoo/fields.py b/odoo/fields.py index 35b2892d447..be097509072 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -157,6 +157,7 @@ class Field(MetaField('DummyField', (object,), {})): :param compute_sudo: whether the field should be recomputed as superuser to bypass access rights (boolean, by default ``False``) + Note that this has no effects on non-stored computed fields The methods given for ``compute``, ``inverse`` and ``search`` are model methods. Their signature is shown in the following example:: diff --git a/odoo/models.py b/odoo/models.py index c9d89c175ed..d73670015c2 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1041,6 +1041,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): except ValidationError as e: raise except Exception as e: + _logger.exception('Exception while validating constraint') raise ValidationError("%s\n\n%s" % (_("Error while validating constraint"), tools.ustr(e))) @api.model diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index 68d19edf046..c73b9352bf3 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -259,7 +259,7 @@ def combine(operator, unit, zero, domains): result += normalize_domain(domain) count += 1 result = [operator] * (count - 1) + result - return result + return result or unit def AND(domains): |
