diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-08-19 18:57:08 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-08-19 18:57:08 +0200 |
| commit | 168e54d48850418a6a83f71427e74ed27f66595b (patch) | |
| tree | 375cb46f5d7692623e570ff3347a07d5b25383d9 | |
| parent | 57fef7c11cea206da4c4919781405913ba1d568f (diff) | |
| parent | 32039b2ab44dd3107c8761187582b6c010f3492c (diff) | |
[MERGE] forward port branch 12.0 up to 32039b2ab44
80 files changed, 555 insertions, 137 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index b82b8756a1c..d20913a5f20 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -2016,7 +2016,7 @@ class AccountInvoiceLine(models.Model): default_tax = self.invoice_id.company_id.account_sale_tax_id else: default_tax = self.invoice_id.company_id.account_purchase_tax_id - self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids or default_tax, partner=self.partner_id).ids + self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids or default_tax, partner=self.partner_id) elif not self.price_unit: self._set_taxes() @@ -2089,7 +2089,7 @@ class AccountInvoiceLine(models.Model): @api.multi def write(self, values): if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')): - raise UserError("You cannot change the type of an invoice line. Instead you should delete the current line and create a new line of the proper type.") + raise UserError(_("You cannot change the type of an invoice line. Instead you should delete the current line and create a new line of the proper type.")) return super(AccountInvoiceLine, self).write(values) _sql_constraints = [ diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 0a64d37bb1e..6676b230e88 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -418,16 +418,23 @@ class AccountMove(models.Model): def assert_balanced(self): if not self.ids: return True - prec = self.env.user.company_id.currency_id.decimal_places - - self._cr.execute("""\ - SELECT move_id - FROM account_move_line - WHERE move_id in %s - GROUP BY move_id - HAVING abs(sum(debit) - sum(credit)) > %s - """, (tuple(self.ids), 10 ** (-max(5, prec)))) - if len(self._cr.fetchall()) != 0: + + # /!\ As this method is called in create / write, we can't make the assumption the computed stored fields + # are already done. Then, this query MUST NOT depend of computed stored fields (e.g. balance). + # It happens as the ORM makes the create with the 'no_recompute' statement. + self._cr.execute(''' + SELECT line.move_id + FROM account_move_line line + JOIN account_move move ON move.id = line.move_id + JOIN account_journal journal ON journal.id = move.journal_id + JOIN res_company company ON company.id = journal.company_id + JOIN res_currency currency ON currency.id = company.currency_id + WHERE line.move_id IN %s + GROUP BY line.move_id, currency.decimal_places + HAVING ROUND(SUM(debit - credit), currency.decimal_places) != 0.0; + ''', [tuple(self.ids)]) + + if self._cr.fetchone(): raise UserError(_("Cannot create unbalanced journal entry.")) return True @@ -674,9 +681,12 @@ class AccountMoveLine(models.Model): help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.") date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines") - tax_ids = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) - tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', compute='_compute_tax_line_id', store=True) - tax_repartition_line_id = fields.Many2one(comodel_name='account.tax.repartition.line', string="Originator Tax Repartition Line", ondelete='restrict', help="Tax repartition line that caused the creation of this move line, if any") + tax_ids = fields.Many2many('account.tax', string='Taxes Applied', domain=['|', ('active', '=', False), ('active', '=', True)], + help="Taxes that apply on the base amount") + tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', compute='_compute_tax_line_id', store=True, + help="Indicates that this journal item is a tax line") + tax_repartition_line_id = fields.Many2one(comodel_name='account.tax.repartition.line', string="Originator Tax Repartition Line", ondelete='restrict', + help="Tax repartition line that caused the creation of this move line, if any") analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', index=True) analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index f90171187ea..511abadd872 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -454,7 +454,10 @@ class account_payment(models.Model): move.line_ids.remove_move_reconcile() move.button_cancel() move.unlink() - rec.state = 'cancelled' + rec.write({ + 'state': 'cancelled', + 'move_name': False, + }) @api.multi def unlink(self): diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 94a077f58b8..8b6054dc878 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -572,6 +572,8 @@ class AccountReconcileModel(models.Model): ''' if not self.match_total_amount: return True + if not candidates: + return False # Match total residual amount. total_residual = 0.0 diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 2988b04843a..eb4a4630d74 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -331,7 +331,7 @@ <field name="module_account_sepa_direct_debit" class="oe_inline" widget="upgrade_boolean"/> </div> <div class="o_setting_right_pane" name="sepa_direct_debit_right_pane"> - <label string="SEPA Direct Debit (SDD)" for="module_account_sepa"/> + <label string="SEPA Direct Debit (SDD)" for="module_account_sepa_direct_debit"/> <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/> <div class="text-muted"> Collect customer payments in one-click using Euro SEPA Service diff --git a/addons/base_iban/models/res_partner_bank.py b/addons/base_iban/models/res_partner_bank.py index f457a912011..a662a9ea959 100644 --- a/addons/base_iban/models/res_partner_bank.py +++ b/addons/base_iban/models/res_partner_bank.py @@ -163,6 +163,7 @@ _map_iban_template = { 'sm': 'SMkk KBBB BBSS SSSC CCCC CCCC CCC', # San Marino 'tn': 'TNkk BBSS SCCC CCCC CCCC CCCC', # Tunisia 'tr': 'TRkk BBBB BRCC CCCC CCCC CCCC CC', # Turkey + 'ua': 'UAkk BBBB BBCC CCCC CCCC CCCC CCCC C', # Ukraine 'vg': 'VGkk BBBB CCCC CCCC CCCC CCCC', # Virgin Islands 'xk': 'XKkk BBBB CCCC CCCC CCCC', # Kosovo } diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 8626d76acb9..04012e07138 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -446,6 +446,9 @@ class Import(models.TransientModel): # Or a date/datetime if it matches the pattern date_patterns = [options['date_format']] if options.get( 'date_format') else [] + user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format + if user_date_format: + date_patterns.append(user_date_format) date_patterns.extend(DATE_PATTERNS) match = check_patterns(date_patterns, preview_values) if match: diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py index 1af76d1d32a..0a39b2d036b 100644 --- a/addons/base_vat/models/res_partner.py +++ b/addons/base_vat/models/res_partner.py @@ -14,6 +14,13 @@ except ImportError: "Install it to support more countries, for example with `easy_install vatnumber`.") vatnumber = None +# Although stdnum is a dependency of vatnumber, the import of the latter is surrounded by a try/except +# if it is not installed. Therefore, we cannot be sure stdnum is installed in all cases. +try: + import stdnum +except ImportError: + stdnum = None + from odoo import api, models, _ from odoo.tools.misc import ustr from odoo.exceptions import ValidationError @@ -63,6 +70,8 @@ _ref_vat = { 'be': 'BE0477472701', 'bg': 'BG1234567892', 'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp + 'cl': 'CL76086428-5', + 'co': 'CO213123432-1 or CO213.123.432-1', 'cy': 'CY12345678F', 'cz': 'CZ12345679', 'de': 'DE123456788', @@ -398,6 +407,12 @@ class ResPartner(models.Model): except ImportError: return True + def check_vat_cl(self, vat): + return stdnum.util.get_cc_module('cl', 'vat').is_valid(vat) if stdnum else True + + def check_vat_co(self, vat): + return stdnum.util.get_cc_module('co', 'vat').is_valid(vat) if stdnum else True + def default_compact(self, vat): return vat diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index 9b5f54d6b1d..1c3c7ab8143 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -122,8 +122,8 @@ <field name="stop" attrs="{'invisible': True}"/> <field name="id" attrs="{'invisible': True}"/> - <field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/> - <field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/> + <field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/> + <field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/> <field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/> <field name="stop_datetime" invisible="1"/> @@ -132,7 +132,7 @@ <field name="duration" widget="float_time" string="Duration" class="oe_inline" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/> <span> hours</span> </div> - <field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/> + <field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/> </group> <group> <field name="categ_ids" widget="many2many_tags" options=" diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index cf2480aa1f5..52ab749665a 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -352,7 +352,7 @@ <div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click"> <div class="o_dropdown_kanban dropdown"> - <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index 6f342b0ad93..eae41d40125 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -317,7 +317,7 @@ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click"> <div class="o_dropdown_kanban dropdown"> - <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index 5f42ca22720..06ad8affdc6 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -194,7 +194,7 @@ <t t-name="kanban-box"> <div class="oe_kanban_card oe_kanban_global_click"> <div class="o_dropdown_kanban dropdown" groups="base.group_user"> - <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index d9f40d24816..59c276f82d9 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -893,24 +893,21 @@ class Escpos: self._raw(PAPER_FULL_CUT) - def cashdraw(self, pin, tries=5): + def cashdraw(self, pin): """ Send pulse to kick the cash drawer - With some printers the drawer will not open after one pulse, for this reason we will check - the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the - drawer status is open, we will stop sending pulses. + For some reason, with some printers (ex: Epson TM-m30), the cash drawer + only opens 50% of the time if you just send the pulse. But if you read + the status afterwards, it opens all the time. """ - for i in range(tries): - if pin == 2: - self._raw(CD_KICK_2) - elif pin == 5: - self._raw(CD_KICK_5) - else: - raise CashDrawerError() - - if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']: - break + if pin == 2: + self._raw(CD_KICK_2) + elif pin == 5: + self._raw(CD_KICK_5) + else: + raise CashDrawerError() + self.get_printer_status() def hw(self, hw): """ Hardware operations """ diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py index 263bf86196a..d6408bbb2cf 100644 --- a/addons/iap/models/iap.py +++ b/addons/iap/models/iap.py @@ -7,7 +7,7 @@ import uuid import werkzeug.urls import requests -from odoo import api, fields, models, exceptions +from odoo import api, fields, models, exceptions, _ from odoo.tools import pycompat _logger = logging.getLogger(__name__) @@ -68,7 +68,9 @@ def jsonrpc(url, method='call', params=None, timeout=15): raise e return response.get('result') except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e: - raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + url) + raise exceptions.AccessError( + _('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was %s') % url + ) #---------------------------------------------------------- # Helpers for proxy diff --git a/addons/l10n_ae/data/account_tax_template_data.xml b/addons/l10n_ae/data/account_tax_template_data.xml index a9ffc836bdb..b3cec92c1c5 100644 --- a/addons/l10n_ae/data/account_tax_template_data.xml +++ b/addons/l10n_ae/data/account_tax_template_data.xml @@ -828,6 +828,17 @@ <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> </record> + <record id="uae_purchase_tax_5_child_reverse" model="account.tax.template"> + <field name="name">Reverse Charge TVA +5%(Purchase)</field> + <field name="type_tax_use">none</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="account_id" ref="uae_account_3726"/> + <field name="refund_account_id" ref="uae_account_3726"/> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> <record id="uae_purchase_tax_reverse_charge" model="account.tax.template"> <field name="name">Reverse Charge Provision</field> <field name="type_tax_use">purchase</field> diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py index 1ba9b6ec213..c78a295a008 100644 --- a/addons/l10n_it_edi/models/account_invoice.py +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -95,7 +95,7 @@ class AccountInvoice(models.Model): # <1.2.1.8> if not seller.l10n_it_tax_system: - raise UserError("The seller's company must have a tax system.") + raise UserError(_("The seller's company must have a tax system.")) # <1.2.2> if not seller.street and not seller.street2: @@ -110,7 +110,7 @@ class AccountInvoice(models.Model): raise UserError(_("%s must have a country.") % (seller.display_name)) # <1.4.1> - if not buyer.vat and not buyer.l10n_it_codice_fiscale: + if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code != 'IT': raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name)) # <1.4.2> @@ -737,7 +737,7 @@ class AccountTax(models.Model): for tax in self: if tax.l10n_it_has_exoneration: if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0: - raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.") + raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.")) if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S': raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'")) diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py index 3bfce47100d..cd83dc55f71 100644 --- a/addons/l10n_it_edi/models/ir_mail_server.py +++ b/addons/l10n_it_edi/models/ir_mail_server.py @@ -35,7 +35,7 @@ class FetchmailServer(models.Model): def _check_pec(self): for record in self: if record.l10n_it_is_pec and record.type != 'imap': - raise ValidationError("PEC mail server must be of type IMAP.") + raise ValidationError(_("PEC mail server must be of type IMAP.")) @api.multi def fetch_mail(self): diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py index a0cce53b9a5..dc170405d92 100644 --- a/addons/l10n_it_edi/models/res_company.py +++ b/addons/l10n_it_edi/models/res_company.py @@ -1,7 +1,7 @@ # -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.exceptions import ValidationError TAX_SYSTEM = [ @@ -94,7 +94,7 @@ class ResCompany(models.Model): or not record.l10n_it_eco_index_share_capital\ or not record.l10n_it_eco_index_sole_shareholder\ or not record.l10n_it_eco_index_liquidation_state: - raise ValidationError("All fields about the Economic and Administrative Index must be completed.") + raise ValidationError(_("All fields about the Economic and Administrative Index must be completed.")) @api.constrains('l10n_it_has_tax_representative', 'l10n_it_tax_representative_partner_id') @@ -103,8 +103,8 @@ class ResCompany(models.Model): if not record.l10n_it_has_tax_representative: continue if not record.l10n_it_tax_representative_partner_id: - raise ValidationError("You must select a tax representative.") + raise ValidationError(_("You must select a tax representative.")) if not record.l10n_it_tax_representative_partner_id.vat: - raise ValidationError("Your tax representative partner must have a tax number.") + raise ValidationError(_("Your tax representative partner must have a tax number.")) if not record.l10n_it_tax_representative_partner_id.country_id: - raise ValidationError("Your tax representative partner must have a country.") + raise ValidationError(_("Your tax representative partner must have a country.")) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 76f316eb3c3..9c3cdbc344a 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -151,23 +151,23 @@ class Channel(models.Model): @api.constrains('moderator_ids') def _check_moderator_email(self): if any(not moderator.email for channel in self for moderator in channel.moderator_ids): - raise ValidationError("Moderators must have an email address.") + raise ValidationError(_("Moderators must have an email address.")) @api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids') def _check_moderator_is_member(self): for channel in self: if not (channel.mapped('moderator_ids.partner_id') <= channel.sudo().channel_partner_ids): - raise ValidationError("Moderators should be members of the channel they moderate.") + raise ValidationError(_("Moderators should be members of the channel they moderate.")) @api.constrains('moderation', 'email_send') def _check_moderation_parameters(self): if any(not channel.email_send and channel.moderation for channel in self): - raise ValidationError('Only mailing lists can be moderated.') + raise ValidationError(_('Only mailing lists can be moderated.')) @api.constrains('moderator_ids') def _check_moderator_existence(self): if any(not channel.moderator_ids for channel in self if channel.moderation): - raise ValidationError('Moderated channels must have moderators.') + raise ValidationError(_('Moderated channels must have moderators.')) @api.multi def _compute_is_member(self): @@ -254,7 +254,7 @@ class Channel(models.Model): # First checks if user tries to modify moderation fields and has not the right to do it. if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation): if not self.env.user.has_group('base.group_system'): - raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.") + raise UserError(_("You do not have the rights to modify fields related to moderation on one of the channels you are modifying.")) tools.image_resize_images(vals) result = super(Channel, self).write(vals) @@ -431,9 +431,9 @@ class Channel(models.Model): if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'): success = self._send_guidelines(self.channel_partner_ids) if not success: - raise UserError('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.') + raise UserError(_('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')) else: - raise UserError("Only an administrator or a moderator can send guidelines to channel members!") + raise UserError(_("Only an administrator or a moderator can send guidelines to channel members!")) @api.multi def _send_guidelines(self, partners): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index dd92247078f..e2d1d046c21 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1222,7 +1222,7 @@ class MailThread(models.AbstractModel): # 1. Check if message is a reply on a thread msg_references = [ref for ref in tools.mail_header_msgid_re.findall(thread_references) if 'reply_to' not in ref] - mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1) + mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1, order='id desc, message_id') is_a_reply = bool(mail_messages) # 1.1 Handle forward to an alias with a different model: do not consider it as a reply @@ -1646,7 +1646,7 @@ class MailThread(models.AbstractModel): # Very unusual situation, be we should be fault-tolerant here message_id = "<%s@localhost>" % time.time() _logger.debug('Parsing Message without message-id, generating a random one: %s', message_id) - msg_dict['message_id'] = message_id + msg_dict['message_id'] = message_id.strip() if message.get('Subject'): msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject')) diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js index b4f1df097cf..0ef02e5f57a 100644 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ b/addons/mail/static/src/js/models/threads/document_thread.js @@ -97,7 +97,6 @@ var DocumentThread = Thread.extend({ fetchMessages: function (options) { var self = this; return this._fetchMessages(options).then(function () { - self.call('mail_service', 'markMessagesAsRead', self._messageIDs); return self._messages; }); }, diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 926b9f9828a..884fc79fee6 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -1146,6 +1146,71 @@ QUnit.test('chatter: access document with some notifs', async function (assert) form.destroy(); }); +QUnit.test('chatter: new message notification from document', async function (assert) { + // when receiving new messages from a document that is open, it should + // not mark the messages are read + assert.expect(4); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + services: this.services, + arch: '<form string="Partners">' + + '<sheet>' + + '<field name="foo"/>' + + '</sheet>' + + '<div class="oe_chatter">' + + '<field name="message_ids" widget="mail_thread"/>' + + '</div>' + + '</form>', + res_id: 2, + session: { + partner_id: 3, + }, + mockRPC: function (route, args) { + if (args.method === 'set_message_done') { + throw new Error("should not mark message as read"); + } + return this._super.apply(this, arguments); + }, + }); + + var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2); + assert.strictEqual( + thread.getUnreadCounter(), + 0, + "document thread should have no unread messages initially"); + assert.containsNone( + form, + '.o_thread_message', + "should have no message in the chatter"); + + // Simulate received needaction`` message on this document + var message = { + author_id: [1, "Me"], + body: '<p>test</p>', + channel_ids: [], + id: 2, + model: 'partner', + needaction_partner_ids: [3], + res_id: 2, + }; + const notifications = [ [['myDB', 'ir.needaction'], message] ]; + form.call('bus_service', 'trigger', 'notification', notifications); + await testUtils.nextMicrotaskTick(); + assert.strictEqual( + thread.getUnreadCounter(), + 1, + "document thread should have one unread message (not marked as read)"); + assert.containsOnce( + form, + '.o_thread_message', + "should have one message in the chatter"); + + form.destroy(); +}); + QUnit.test('chatter: post a message and switch in edit mode', async function (assert) { assert.expect(5); diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py index 12255735871..561597509f2 100644 --- a/addons/mail/wizard/mail_resend_message.py +++ b/addons/mail/wizard/mail_resend_message.py @@ -48,7 +48,7 @@ class MailResendMessage(models.TransientModel): rec['mail_message_id'] = mail_message_id.id rec['partner_ids'] = partner_ids else: - raise UserError('No message_id found in context') + raise UserError(_('No message_id found in context')) return rec @api.multi diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 6f7ace29554..710b0c73e7c 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -43,7 +43,7 @@ class MaintenanceEquipmentCategory(models.Model): alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete='restrict', required=True, help="Email alias for this equipment category. New emails will automatically " - "create new maintenance request for this equipment category.") + "create a new equipment under this category.") fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True) @api.multi diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 0e89fa52009..5dc67817fea 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -136,7 +136,7 @@ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_semantic_html_override"> <div class="o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> @@ -435,7 +435,7 @@ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click"> <div class="o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index a00538f04a2..44dab1bf056 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -253,7 +253,7 @@ class MassMailingContact(models.Model): contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)]) return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] else: - raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.') + raise UserError(_('Search opt out cannot be executed without a unique and valid active mailing list context.')) @api.depends('subscription_list_ids') def _compute_opt_out(self): diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index aa556f1ee4c..d90a178f4e2 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -386,7 +386,7 @@ </div> <div class="o_kanban_card_manage_pane"> <div class="o_kanban_card_manage_section o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> @@ -685,7 +685,7 @@ <t t-name="kanban-box"> <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing"> <div class="o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> @@ -1041,7 +1041,7 @@ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign"> <div class="o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/mass_mailing/views/res_config_settings_views.xml b/addons/mass_mailing/views/res_config_settings_views.xml index a8a56b27461..63626781ea1 100644 --- a/addons/mass_mailing/views/res_config_settings_views.xml +++ b/addons/mass_mailing/views/res_config_settings_views.xml @@ -58,7 +58,7 @@ </div> </div> </div> - <h2>Mailing Lists</h2> + <h2 id="mailing_list_settings_title" invisible="1">Mailing Lists</h2> <div id="mailing_list" class="row mt16 o_settings_container"/> </div> </xpath> diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 81ef7c99da2..7b4004a2a6e 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -213,6 +213,11 @@ class MrpWorkorder(models.Model): for order in (self - late_orders): order.color = 2 + @api.onchange('date_planned_start', 'duration_expected') + def _onchange_date_planned_finished(self): + if self.date_planned_start and self.duration_expected: + self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected) + @api.multi def write(self, values): if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self): diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml index 73052c91208..636ee773c95 100644 --- a/addons/note/views/note_views.xml +++ b/addons/note/views/note_views.xml @@ -98,7 +98,7 @@ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/> <div class="o_dropdown_kanban dropdown"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/partner_autocomplete/models/res_partner.py b/addons/partner_autocomplete/models/res_partner.py index 1f5a9942c93..0e42da447c6 100644 --- a/addons/partner_autocomplete/models/res_partner.py +++ b/addons/partner_autocomplete/models/res_partner.py @@ -110,7 +110,7 @@ class ResPartner(models.Model): if suggestions: results = [] for suggestion in suggestions: - results.append(suggestion) + results.append(self._format_data_company(suggestion)) return results else: return [] diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 78b7d2947eb..6f3b4e05bbe 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -76,7 +76,10 @@ var ScreenWidget = PosBaseWidget.extend({ barcode_client_action: function(code){ var partner = this.pos.db.get_partner_by_barcode(code.code); if(partner){ - this.pos.get_order().set_client(partner); + if (this.pos.get_order().get_client() !== partner) { + this.pos.get_order().set_client(partner); + this.pos.get_order().set_pricelist(_.findWhere(this.pos.pricelists, {'id': partner.property_product_pricelist[0]}) || this.pos.default_pricelist); + } return true; } this.barcode_error_action(code); diff --git a/addons/point_of_sale/wizard/pos_details.xml b/addons/point_of_sale/wizard/pos_details.xml index 596ba509825..6a10896b6fd 100644 --- a/addons/point_of_sale/wizard/pos_details.xml +++ b/addons/point_of_sale/wizard/pos_details.xml @@ -10,7 +10,7 @@ <field name="end_date"/> </group> <group> - <field name="pos_config_ids" colspan="4" nolabel="1" /> + <field name="pos_config_ids" mode="tree" colspan="4" nolabel="1" /> </group> <footer> <button name="generate_report" string="Print" type="object" class="btn-primary"/> diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index 2efa54b5c17..4af3da9c8d0 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -244,7 +244,7 @@ class CustomerPortal(Controller): document = request.env[model_name].browse([document_id]) document_sudo = document.sudo().exists() if not document_sudo: - raise MissingError("This document does not exist.") + raise MissingError(_("This document does not exist.")) try: document.check_access_rights('read') document.check_access_rule('read') @@ -279,12 +279,12 @@ class CustomerPortal(Controller): def _show_report(self, model, report_type, report_ref, download=False): if report_type not in ('html', 'pdf', 'text'): - raise UserError("Invalid report type: %s" % report_type) + raise UserError(_("Invalid report type: %s") % report_type) report_sudo = request.env.ref(report_ref).sudo() if not isinstance(report_sudo, type(request.env['ir.actions.report'])): - raise UserError("%s is not the reference of a report" % report_ref) + raise UserError(_("%s is not the reference of a report") % report_ref) method_name = 'render_qweb_%s' % (report_type) report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0] diff --git a/addons/pos_mercury/views/pos_config_setting_views.xml b/addons/pos_mercury/views/pos_config_setting_views.xml index 99f0f9d786f..cc92de56eea 100644 --- a/addons/pos_mercury/views/pos_config_setting_views.xml +++ b/addons/pos_mercury/views/pos_config_setting_views.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view"> - <field name="name">pos.config.form.inherit.mercury</field> + <record id="res_config_view_form_inherit_pos_mercury" model="ir.ui.view"> + <field name="name">res.config.form.inherit.mercury</field> <field name="model">res.config.settings</field> <field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/> <field name="arch" type="xml"> diff --git a/addons/pos_mercury/views/pos_config_views.xml b/addons/pos_mercury/views/pos_config_views.xml index 8da1d16bd8a..63aa77de208 100644 --- a/addons/pos_mercury/views/pos_config_views.xml +++ b/addons/pos_mercury/views/pos_config_views.xml @@ -1,6 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view"> + <!-- TODO the inherit for + point_of_sale.res_config_settings_view_form in this module + uses the pos_config_view_form_inherit_pos_mercury xml id + already. It should be renamed to + res_config_settings_view_form_inherit_pos_mercury. --> + <record id="pos_config_view_form_inherit_pos_mercury_2" model="ir.ui.view"> <field name="name">pos.config.form.view.inherit.mercury</field> <field name="model">pos.config</field> <field name="inherit_id" ref="point_of_sale.pos_config_view_form"/> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index d9969a3e205..023da36d9cb 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -565,7 +565,7 @@ </small> </div> <div class="o_dropdown_kanban dropdown" groups="base.group_user"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index bd9cf07f558..651ab3be0c0 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -404,6 +404,8 @@ class PurchaseOrder(models.Model): 'default_company_id': self.company_id.id, 'company_id': self.company_id.id } + if self.user_id: + result['context']['default_user_id'] = self.user_id.id # choose the view_mode accordingly if len(self.invoice_ids) > 1 and not create_bill: result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]" diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py index e5c018ded62..bd6d2081ece 100644 --- a/addons/purchase_stock/models/stock.py +++ b/addons/purchase_stock/models/stock.py @@ -197,7 +197,8 @@ class ProductionLot(models.Model): stock_moves = self.env['stock.move.line'].search([ ('lot_id', '=', lot.id), ('state', '=', 'done') - ]).mapped('move_id').filtered( + ]).mapped('move_id') + stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered( lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done') lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id') lot.purchase_order_count = len(lot.purchase_order_ids) diff --git a/addons/purchase_stock/tests/test_create_picking.py b/addons/purchase_stock/tests/test_create_picking.py index 8a2e6c530c7..871487a5144 100644 --- a/addons/purchase_stock/tests/test_create_picking.py +++ b/addons/purchase_stock/tests/test_create_picking.py @@ -313,3 +313,65 @@ class TestCreatePicking(common.TestProductCommon): purchase_order_2.picking_ids.button_validate() self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.') + + def test_04_rounding(self): + """ We set the Unit(s) rounding to 1.0 and ensure buying 1.2 units in a PO is rounded to 1.0 + at reception. + """ + uom_unit = self.env.ref('uom.product_uom_unit') + uom_unit.rounding = 1.0 + + # buy a dozen + po = self.env['purchase.order'].create(self.po_vals) + + po.order_line.product_qty = 1.2 + po.button_confirm() + + # the move should be 1.0 units + move1 = po.picking_ids.move_lines[0] + self.assertEqual(move1.product_uom_qty, 1.0) + self.assertEqual(move1.product_uom.id, uom_unit.id) + self.assertEqual(move1.product_qty, 1.0) + + # edit the so line, buy 2.4 units, the move should now be 2.0 units + po.order_line.product_qty = 2.0 + self.assertEqual(move1.product_uom_qty, 2.0) + self.assertEqual(move1.product_uom.id, uom_unit.id) + self.assertEqual(move1.product_qty, 2.0) + + # deliver everything + move1.quantity_done = 2.0 + po.picking_ids.button_validate() + + # check the delivered quantity + self.assertEqual(po.order_line.qty_received, 2.0) + + def test_05_uom_rounding(self): + """ We set the Unit(s) and Dozen(s) rounding to 1.0 and ensure buying 1.3 dozens in a PO is + rounded to 1.0 at reception. + """ + uom_unit = self.env.ref('uom.product_uom_unit') + uom_dozen = self.env.ref('uom.product_uom_dozen') + uom_unit.rounding = 1.0 + uom_dozen.rounding = 1.0 + + # buy 1.3 dozen + po = self.env['purchase.order'].create(self.po_vals) + + po.order_line.product_qty = 1.3 + po.order_line.product_uom = uom_dozen.id + po.button_confirm() + + # the move should be 16.0 units + move1 = po.picking_ids.move_lines[0] + self.assertEqual(move1.product_uom_qty, 16.0) + self.assertEqual(move1.product_uom.id, uom_unit.id) + self.assertEqual(move1.product_qty, 16.0) + + # force the propagation of the uom, buy 2.6 dozens, the move 2 should have 2 dozens + self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1') + po.order_line.product_qty = 2.6 + move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id) + self.assertEqual(move2.product_uom_qty, 2) + self.assertEqual(move2.product_uom.id, uom_dozen.id) + self.assertEqual(move2.product_qty, 24) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 06d1a37ae40..e951e03c15c 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1097,7 +1097,7 @@ class SaleOrderLine(models.Model): @api.multi def write(self, values): if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')): - raise UserError("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type.") + raise UserError(_("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type.")) if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py index 493386f33c7..71c4547d1ba 100644 --- a/addons/sale_management/models/sale_order_template.py +++ b/addons/sale_management/models/sale_order_template.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError @@ -36,7 +36,7 @@ class SaleOrderTemplate(models.Model): template_id = self.env['ir.default'].get('sale.order', 'sale_order_template_id') for template in self: if template_id and template_id == template.id: - raise UserError('Before archiving "%s" please select another default template in the settings.' % template.name) + raise UserError(_('Before archiving "%s" please select another default template in the settings.') % template.name) return super(SaleOrderTemplate, self).write(vals) @@ -87,7 +87,7 @@ class SaleOrderTemplateLine(models.Model): @api.multi def write(self, values): if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')): - raise UserError("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.") + raise UserError(_("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.")) return super(SaleOrderTemplateLine, self).write(values) _sql_constraints = [ diff --git a/addons/sale_management/views/sale_order_views.xml b/addons/sale_management/views/sale_order_views.xml index f18f8a429c4..e5af22808eb 100644 --- a/addons/sale_management/views/sale_order_views.xml +++ b/addons/sale_management/views/sale_order_views.xml @@ -98,12 +98,13 @@ </field> </record> + <!-- TODO remove me after saas-12.4 to be replaced by sale.prod_config_main --> <menuitem id="menu_product_settings" name="Products" sequence="80" parent="sale.menu_sale_config"/> <menuitem id="menu_product_attribute_action" action="product.attribute_action" - parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/> + parent="sale.prod_config_main" groups="product.group_product_variant" sequence="1"/> <menuitem id="menu_catalog_variants_action" action="product.variants_action" - parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/> + parent="sale.prod_config_main" groups="product.group_product_variant" sequence="2"/> </odoo> diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 36c2795e61d..b85d4525566 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -123,7 +123,8 @@ class ProductionLot(models.Model): stock_moves = self.env['stock.move.line'].search([ ('lot_id', '=', lot.id), ('state', '=', 'done') - ]).mapped('move_id').filtered( + ]).mapped('move_id') + stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered( lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done') lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id') lot.sale_order_count = len(lot.sale_order_ids) diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index 12a18f5524b..dbae7d52522 100644 --- a/addons/sale_timesheet/models/account_invoice.py +++ b/addons/sale_timesheet/models/account_invoice.py @@ -61,7 +61,7 @@ class AccountInvoiceLine(models.Model): sale_line_delivery = invoice_line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet') if sale_line_delivery: domain = self._timesheet_domain_get_invoiced_lines(sale_line_delivery) - timesheets = self.env['account.analytic.line'].search(domain) + timesheets = self.env['account.analytic.line'].search(domain).sudo() timesheets.write({ 'timesheet_invoice_id': invoice_line.invoice_id.id, }) diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index f0c34023b5e..d4fae7b5a6e 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -90,7 +90,8 @@ class SaleOrder(models.Model): task_projects = self.tasks_ids.mapped('project_id') if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...) - action = self.env.ref('project.act_project_project_2_project_task_all').read()[0] + action = self.with_context(active_id=task_projects.id).env.ref( + 'project.act_project_project_2_project_task_all').read()[0] if action.get('context'): eval_context = self.env['ir.actions.actions']._get_eval_context() eval_context.update({'active_id': task_projects.id}) diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py index f5116ebf7e6..3eca680cce7 100644 --- a/addons/stock/models/res_config_settings.py +++ b/addons/stock/models/res_config_settings.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo import api, fields, models class ResConfigSettings(models.TransientModel): @@ -65,13 +64,6 @@ class ResConfigSettings(models.TransientModel): def _onchange_group_stock_production_lot(self): if not self.group_stock_production_lot: self.group_lot_on_delivery_slip = False - tracked_products = self.env['product.template'].search([('tracking', 'in', ['lot', 'serial']),]) - if not self.group_stock_production_lot and tracked_products: - names = ", ".join(tracked_products.mapped('display_name') if len(tracked_products) <= 10 - else tracked_products[:10].mapped('display_name') + ["..."]) - raise UserError(_("You should not remove the 'lots and serial numbers' " - "option while the following products are still tracked by lot " - "or serial number:\n %s") % names) @api.onchange('group_stock_adv_location') def onchange_adv_location(self): diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index 43cbf15996c..7686051ecc4 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -43,7 +43,7 @@ class ProductionLot(models.Model): @api.multi def write(self, vals): if 'product_id' in vals and any([vals['product_id'] != lot.product_id.id for lot in self]): - move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids)]) + move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids), ('product_id', '!=', vals['product_id'])]) if move_lines: raise UserError(_( 'You are not allowed to change the product linked to a serial or lot number ' + diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index b90a981f668..f7945dffa10 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -677,8 +677,8 @@ class StockMove(models.Model): location_from = self.location_id location_to = self.location_dest_id - company_from = self._is_out() and self.mapped('move_line_ids.location_id.company_id') or False - company_to = self._is_in() and self.mapped('move_line_ids.location_dest_id.company_id') or False + company_from = self.mapped('move_line_ids.location_id.company_id') if self._is_out() else False + company_to = self.mapped('move_line_ids.location_dest_id.company_id') if self._is_in() else False # Create Journal Entry for products arriving in the company; in case of routes making the link between several # warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries diff --git a/addons/stock_picking_batch/report/report_picking_batch.xml b/addons/stock_picking_batch/report/report_picking_batch.xml index 4eb4cf2ec62..db86d4dc866 100644 --- a/addons/stock_picking_batch/report/report_picking_batch.xml +++ b/addons/stock_picking_batch/report/report_picking_batch.xml @@ -32,7 +32,7 @@ <span t-field="pick.name"/> </td> <td> - <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pick.name, 600, 100)" style="width:300px;height:50px" alt="Barcode"/> </td> <td> <span t-field="pick.state"/> diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml index 43a7a44c26c..ae6960bfa99 100644 --- a/addons/survey/views/survey_survey_views.xml +++ b/addons/survey/views/survey_survey_views.xml @@ -136,7 +136,7 @@ <div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click o_kanban_card_survey"> <div class="o_dropdown_kanban dropdown" t-if="widget.editable"> - <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu"> + <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu"> diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index 44e09933a25..da9e557ae4f 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -500,7 +500,7 @@ var utils = { } var normalized_value = value / precision; var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2); - var epsilon = Math.pow(2, epsilon_magnitude - 53); + var epsilon = Math.pow(2, epsilon_magnitude - 52); normalized_value += normalized_value >= 0 ? epsilon : -epsilon; /** diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 1a7f0b5f079..3bd11a9722a 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1362,9 +1362,9 @@ var BasicModel = AbstractModel.extend({ // apply changes to local data for (var fieldName in changes) { field = record.fields[fieldName]; - if (field.type === 'one2many' || field.type === 'many2many') { + if (field && (field.type === 'one2many' || field.type === 'many2many')) { defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options)); - } else if (field.type === 'many2one' || field.type === 'reference') { + } else if (field && (field.type === 'many2one' || field.type === 'reference')) { defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName])); } else { record._changes[fieldName] = changes[fieldName]; @@ -1379,7 +1379,7 @@ var BasicModel = AbstractModel.extend({ var onChangeFields = []; // the fields that have changed and that have an on_change for (var fieldName in changes) { field = record.fields[fieldName]; - if (field.onChange) { + if (field && field.onChange) { var isX2Many = field.type === 'one2many' || field.type === 'many2many'; if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) { onChangeFields.push(fieldName); diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index eae16968833..dfc86042aca 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -91,10 +91,15 @@ KanbanRenderer.include({ var index = _.findIndex(this.widgets, {db_id: localID}); var $column = this.widgets[index].$el; var left = $column.css('left'); + var right = $column.css('right'); var scrollTop = $column.scrollTop(); return this._super.apply(this, arguments).then(function () { $column = self.widgets[index].$el; - $column.css({left: left}); + if (_t.database.parameters.direction === 'rtl') { + $column.css({right: right}); + } else { + $column.css({left: left}); + } $column.scrollTop(scrollTop); // required when clicking on 'Load More' self._enableSwipe(); }); @@ -115,19 +120,40 @@ KanbanRenderer.include({ var self = this; var moveToIndex = this.activeColumnIndex; var updateFunc = animate ? 'animate' : 'css'; + var rtl = _t.database.parameters.direction === 'rtl'; _.each(this.widgets, function (column, index) { var columnID = column.id || column.db_id; var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); if (index === moveToIndex - 1) { - $column[updateFunc]({left: '-100%'}); + if (rtl) { + $column[updateFunc]({right: '-100%'}); + } else { + $column[updateFunc]({left: '-100%'}); + } } else if (index === moveToIndex + 1) { - $column[updateFunc]({left: '100%'}); + if (rtl) { + $column[updateFunc]({right: '100%'}); + } else { + $column[updateFunc]({left: '100%'}); + } } else if (index === moveToIndex) { - $column[updateFunc]({left: '0%'}); + if (rtl) { + $column[updateFunc]({right: '0%'}); + } else { + $column[updateFunc]({left: '0%'}); + } } else if (index < moveToIndex) { - $column.css({left: '-100%'}); + if (rtl) { + $column.css({right: '-100%'}); + } else { + $column.css({left: '-100%'}); + } } else if (index > moveToIndex) { - $column.css({left: '100%'}); + if (rtl) { + $column.css({right: '100%'}); + } else { + $column.css({left: '100%'}); + } } }); } @@ -184,6 +210,7 @@ KanbanRenderer.include({ } } // Apply the scroll x on the tabs + // XXX in case of RTL, should we use scrollRight? this.$('.o_kanban_mobile_tabs').scrollLeft(scrollToLeft); } }, @@ -213,16 +240,17 @@ KanbanRenderer.include({ */ _enableSwipe: function () { var self = this; + var step = _t.database.parameters.direction === 'rtl' ? -1 : 1; this.$el.swipe({ excludedElements: ".o_kanban_mobile_tabs", swipeLeft: function () { - var moveToIndex = self.activeColumnIndex + 1; + var moveToIndex = self.activeColumnIndex + step; if (moveToIndex < self.widgets.length) { self._moveToGroup(moveToIndex, self.ANIMATE); } }, swipeRight: function () { - var moveToIndex = self.activeColumnIndex - 1; + var moveToIndex = self.activeColumnIndex - step; if (moveToIndex > -1) { self._moveToGroup(moveToIndex, self.ANIMATE); } diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js index 52bfa60a180..2d3d7aa247d 100644 --- a/addons/web/static/tests/views/kanban_mobile_tests.js +++ b/addons/web/static/tests/views/kanban_mobile_tests.js @@ -1,10 +1,12 @@ odoo.define('web.kanban_mobile_tests', function (require) { "use strict"; +var core = require('web.core'); var KanbanView = require('web.KanbanView'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; +var _t = core._t; QUnit.module('Views', { beforeEach: function () { @@ -119,6 +121,35 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('mobile grouped rendering in rtl direction', async function (assert) { + assert.expect(2); + + var direction = _t.database.parameters.direction; + _t.database.parameters.direction = 'rtl'; + + var kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test o_kanban_small_column" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates>' + + '</kanban>', + domain: [['product_id', '!=', false]], + groupBy: ['product_id'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first')[0].style.right, '0%', + "first tab should have 50% right"); + assert.strictEqual(kanban.$('.o_kanban_group:nth(1)')[0].style.right, '100%', + "second tab should have 100% right"); + + kanban.destroy(); + _t.database.parameters.direction = direction; + }); + + QUnit.test('mobile grouped with undefined column', async function (assert) { assert.expect(5); diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py index 1d46b55c3bd..0422e6f0e29 100644 --- a/addons/website/models/mixins.py +++ b/addons/website/models/mixins.py @@ -102,7 +102,12 @@ class WebsiteMultiMixin(models.AbstractModel): _name = 'website.multi.mixin' _description = 'Multi Website Mixin' - website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.') + website_id = fields.Many2one( + "website", + string="Website", + ondelete="restrict", + help="Restrict publishing to this website.", + ) @api.multi def can_access_from_current_website(self, website_id=False): diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 82e4f36107f..1771b299c7a 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -152,6 +152,19 @@ class Website(models.Model): self.env['ir.qweb'].clear_caches() return result + @api.multi + def unlink(self): + # Do not delete invoices, delete what's strictly necessary + attachments_to_unlink = self.env['ir.attachment'].search([ + ('website_id', 'in', self.ids), + '|', '|', + ('key', '!=', False), # theme attachment + ('url', 'ilike', '.custom.'), # customized theme attachment + ('url', 'ilike', '.assets\\_'), + ]) + attachments_to_unlink.unlink() + return super(Website, self).unlink() + # ---------------------------------------------------------- # Page Management # ---------------------------------------------------------- diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py index 4afb9bd63e5..4d3e89c2347 100644 --- a/addons/website/models/website_menu.py +++ b/addons/website/models/website_menu.py @@ -21,7 +21,7 @@ class Menu(models.Model): page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade') new_window = fields.Boolean('New Window') sequence = fields.Integer(default=_default_sequence) - website_id = fields.Many2one('website', 'Website') + website_id = fields.Many2one('website', 'Website', ondelete='cascade') parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade") child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus') parent_path = fields.Char(index=True) diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py index d73a525e791..4cd5efcb79c 100644 --- a/addons/website/tests/test_ui.py +++ b/addons/website/tests/test_ui.py @@ -6,6 +6,58 @@ import odoo.tests @odoo.tests.tagged('-at_install', 'post_install') +class TestUiCustomizeTheme(odoo.tests.HttpCase): + def test_01_attachment_website_unlink(self): + ''' Some ir.attachment needs to be unlinked when a website is unlink, + otherwise some flows will just crash. That's the case when 2 website + have their theme color customized. Removing a website will make its + customized attachment generic, thus having 2 attachments with the + same URL available for other websites, leading to singleton errors + (among other). + + But no all attachment should be deleted, eg we don't want to delete + a SO or invoice PDF coming from an ecommerce order. + ''' + Website = self.env['website'] + Page = self.env['website.page'] + Attachment = self.env['ir.attachment'] + + website_default = Website.browse(1) + website_test = Website.create({'name': 'Website Test'}) + + # simulate attachment state when editing 2 theme through customize + custom_url = '/TEST/website/static/src/scss/options/colors/user_theme_color_palette.custom.web.assets_common.scss' + scss_attachment = Attachment.create({ + 'name': custom_url, + 'type': 'binary', + 'mimetype': 'text/scss', + 'datas': '', + 'datas_fname': custom_url, + 'url': custom_url, + 'website_id': website_default.id + }) + scss_attachment.copy({'website_id': website_test.id}) + + # simulate PDF from ecommerce order + # Note: it will only have its website_id flag if the website has a domain + # equal to the current URL (fallback or get_current_website()) + so_attachment = Attachment.create({ + 'name': 'SO036.pdf', + 'type': 'binary', + 'mimetype': 'application/pdf', + 'datas': '', + 'website_id': website_test.id + }) + + # avoid sql error on page website_id restrict + Page.search([('website_id', '=', website_test.id)]).unlink() + website_test.unlink() + self.assertEqual(Attachment.search_count([('url', '=', custom_url)]), 1, 'Should not left duplicates when deleting a website') + self.assertTrue(so_attachment.exists(), 'Most attachment should not be deleted') + self.assertFalse(so_attachment.website_id, 'Website should be removed') + + +@odoo.tests.tagged('-at_install', 'post_install') class TestUiHtmlEditor(odoo.tests.HttpCase): def test_html_editor_multiple_templates(self): Website = self.env['website'] diff --git a/addons/website_event/static/src/js/website_geolocation.js b/addons/website_event/static/src/js/website_geolocation.js index 19ee2f87033..5e81eb0adce 100644 --- a/addons/website_event/static/src/js/website_geolocation.js +++ b/addons/website_event/static/src/js/website_geolocation.js @@ -12,12 +12,22 @@ publicWidget.registry.visitor = publicWidget.Widget.extend({ start: function () { var defs = [this._super.apply(this, arguments)]; var self = this; + var $eventList = this.$('.country_events_list'); + this._originalContent = $eventList[0].outerHTML; defs.push(this._rpc({route: '/event/get_country_event_list'}).then(function (data) { if (data) { - self.$('.country_events_list').replaceWith(data); + self._$loadedContent = $(data); + $eventList.replaceWith(self._$loadedContent); } })); return Promise.all(defs); }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + this._$loadedContent.replaceWith(this._originalContent); + }, }); }); diff --git a/addons/website_mass_mailing/views/res_config_settings_views.xml b/addons/website_mass_mailing/views/res_config_settings_views.xml index 16cbcbeda83..5ba37f54171 100644 --- a/addons/website_mass_mailing/views/res_config_settings_views.xml +++ b/addons/website_mass_mailing/views/res_config_settings_views.xml @@ -5,6 +5,9 @@ <field name="name">res.config.settings.view.form.inherit.website.mass.mailing</field> <field name="model">res.config.settings</field> <field name="arch" type="xml"> + <xpath expr="//h2[@id='mailing_list_settings_title']" position="attributes"> + <attribute name="invisible">0</attribute> + </xpath> <xpath expr="//div[@id='mailing_list']" position="inside"> <div class="col-lg-6 o_setting_box col-12" title="This adds a new Newsletter Popup snippet to drag & drop on your website pages. It triggers a popup when visitors land on the page. This popup prompts them to enter their email address to join a mailing list."> <div class="o_setting_left_pane"> diff --git a/addons/website_payment/models/payment_acquirer.py b/addons/website_payment/models/payment_acquirer.py index fe0c00f9a26..2bd0dca903c 100644 --- a/addons/website_payment/models/payment_acquirer.py +++ b/addons/website_payment/models/payment_acquirer.py @@ -6,4 +6,4 @@ from odoo import fields, models class PaymentAcquirer(models.Model): _inherit = 'payment.acquirer' - website_id = fields.Many2one('website') + website_id = fields.Many2one('website', ondelete='restrict') diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 3705d7b3cb3..cca2bda9fa4 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -30,7 +30,7 @@ class ProductPricelist(models.Model): domain = [('company_id', '=', company_id)] return self.env['website'].search(domain, limit=1) - website_id = fields.Many2one('website', string="Website", default=_default_website) + website_id = fields.Many2one('website', string="Website", ondelete='restrict', default=_default_website) code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user") selectable = fields.Boolean(help="Allow the end user to choose this price list") diff --git a/addons/website_sale_comparison/controllers/main.py b/addons/website_sale_comparison/controllers/main.py index f6cb6cfc0fe..a2dac3d4904 100644 --- a/addons/website_sale_comparison/controllers/main.py +++ b/addons/website_sale_comparison/controllers/main.py @@ -18,14 +18,15 @@ class WebsiteSaleProductComparison(WebsiteSale): products = request.env['product.product'].search([('id', 'in', product_ids)]) values['products'] = products.with_context(display_default_code=False) - res = {} + res = OrderedDict() + attrs = products.mapped('product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id').filtered(lambda x: x.create_variant) + for attr in attrs.sorted(lambda att: (att.category_id.sequence, att.sequence)): + cat_name = attr.category_id.name or _('Uncategorized') + res.setdefault(cat_name, OrderedDict()).setdefault(attr.name, [' - '] * len(products)) for num, product in enumerate(products): - for var in product.product_tmpl_id._get_valid_product_template_attribute_lines(): + for var in product.product_tmpl_id.valid_product_template_attribute_line_ids.filtered(lambda x: x.attribute_id.create_variant): cat_name = var.attribute_id.category_id.name or _('Uncategorized') att_name = var.attribute_id.name - if not var.attribute_id.create_variant: - continue - res.setdefault(cat_name, OrderedDict()).setdefault(att_name, [' - '] * len(products)) val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id) if val: res[cat_name][att_name][num] = val[0].name diff --git a/addons/website_sale_wishlist/models/product_wishlist.py b/addons/website_sale_wishlist/models/product_wishlist.py index 0a67caa597c..d456cecd8fb 100644 --- a/addons/website_sale_wishlist/models/product_wishlist.py +++ b/addons/website_sale_wishlist/models/product_wishlist.py @@ -18,7 +18,7 @@ class ProductWishlist(models.Model): currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', readonly=True) pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', help='Pricelist when added') price = fields.Monetary(digits=0, currency_field='currency_id', string='Price', help='Price of the product when it has been added in the wishlist') - website_id = fields.Many2one('website', required=True) + website_id = fields.Many2one('website', ondelete='cascade', required=True) active = fields.Boolean(default=True, required=True) @api.model diff --git a/addons/website_twitter/models/website_twitter_tweet.py b/addons/website_twitter/models/website_twitter_tweet.py index 4e80e439296..6fa6fb1564c 100644 --- a/addons/website_twitter/models/website_twitter_tweet.py +++ b/addons/website_twitter/models/website_twitter_tweet.py @@ -8,7 +8,7 @@ class WebsiteTwitterTweet(models.Model): _name = 'website.twitter.tweet' _description = 'Website Twitter' - website_id = fields.Many2one('website', string='Website') + website_id = fields.Many2one('website', string='Website', ondelete='cascade') screen_name = fields.Char(string='Screen Name') tweet = fields.Text(string='Tweets') diff --git a/doc/cla/individual/igoris.md b/doc/cla/individual/igoris.md new file mode 100644 index 00000000000..c4db9c166d6 --- /dev/null +++ b/doc/cla/individual/igoris.md @@ -0,0 +1,11 @@ +Ukraine, 2019-08-12 + +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, + +Igor Ishchenko igor@cruxlab.com https://github.com/igoris diff --git a/doc/cla/individual/reinaldoramosarxi.md b/doc/cla/individual/reinaldoramosarxi.md new file mode 100644 index 00000000000..b130c0778a0 --- /dev/null +++ b/doc/cla/individual/reinaldoramosarxi.md @@ -0,0 +1,11 @@ +Portugal, 2019-08-09 + +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, + +Reinaldo Ramos 42942314+reinaldoramosarxi@users.noreply.github.com https://github.com/reinaldoramosarxi diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index f4f464f7aa9..5d910d3b061 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -458,12 +458,16 @@ template inheritance '''''''''''''''''''' Template inheritance is used to alter existing templates in-place, e.g. to -add information to templates created by an other modules. +add information to templates created by other modules. Template inheritance is performed via the ``t-extend`` directive which takes the name of the template to alter as parameter. -The alteration is then performed with any number of ``t-jquery`` +When ``t-extend`` is combined with ``t-name`` a new template with the given name +is created. In this case the extended template is not altered, instead the +directives define how to create the new template. + +In both cases the alteration is then performed with any number of ``t-jquery`` sub-directives:: <t t-extend="base.template"> diff --git a/odoo/__init__.py b/odoo/__init__.py index 6bfe00b8499..0080ccd41c9 100644 --- a/odoo/__init__.py +++ b/odoo/__init__.py @@ -63,6 +63,25 @@ if hasattr(time, 'tzset'): time.tzset() #---------------------------------------------------------- +# PyPDF2 hack +# ensure that zlib does not throw error -5 when decompressing +# because some pdf won't fit into allocated memory +# https://docs.python.org/3/library/zlib.html#zlib.decompressobj +# ---------------------------------------------------------- +import PyPDF2 + +try: + import zlib + + def _decompress(data): + zobj = zlib.decompressobj() + return zobj.decompress(data) + + PyPDF2.filters.decompress = _decompress +except ImportError: + pass # no fix required + +#---------------------------------------------------------- # Shortcuts #---------------------------------------------------------- # The hard-coded super-user id (a.k.a. administrator, or root user). diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv index 7aa92271ccd..9715e988fe4 100644 --- a/odoo/addons/base/data/res.lang.csv +++ b/odoo/addons/base/data/res.lang.csv @@ -51,6 +51,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",", "base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d. %b %Y","%H:%M:%S","True","1" "base.lang_fa_IR","Persian / فارس","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6" "base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1" +"base.lang_pt_AO","Portuguese (AO) / Português (AO)","pt_AO","pt_AO","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1" "base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7" "base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1" "base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True","1" diff --git a/odoo/addons/base/models/ir_rule.py b/odoo/addons/base/models/ir_rule.py index e0ee6587b7c..a3fac4c27a3 100644 --- a/odoo/addons/base/models/ir_rule.py +++ b/odoo/addons/base/models/ir_rule.py @@ -44,7 +44,9 @@ class IrRule(models.Model): def _eval_context(self): """Returns a dictionary to use as evaluation context for ir.rule domains.""" - return {'user': self.env.user, 'time': time} + # use an empty context for 'user' to make the domain evaluation + # independent from the context + return {'user': self.env.user.with_context({}), 'time': time} @api.depends('groups') def _compute_global(self): diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index bb017902437..b524a6770ea 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -887,7 +887,7 @@ class Users(models.Model): cfg = self.env['ir.config_parameter'].sudo() min_failures = int(cfg.get_param('base.login_cooldown_after', 5)) if min_failures == 0: - return True + return False delay = int(cfg.get_param('base.login_cooldown_duration', 60)) return failures >= min_failures and (datetime.datetime.now() - previous) < datetime.timedelta(seconds=delay) diff --git a/odoo/addons/base/tests/test_float.py b/odoo/addons/base/tests/test_float.py index 4dbf1f5f139..5bfdbea0a92 100644 --- a/odoo/addons/base/tests/test_float.py +++ b/odoo/addons/base/tests/test_float.py @@ -27,6 +27,8 @@ class TestFloatPrecision(TransactionCase): try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down try_round(0.005,'0.01') # the rule is to round half away from zero try_round(-0.005,'-0.01') # the rule is to round half away from zero + try_round(6.6 * 0.175, '1.16') # 6.6 * 0.175 is rounded to 1.15 with epsilon = 53 + try_round(-6.6 * 0.175, '-1.16') def try_zero(amount, expected): self.assertEqual(currency.is_zero(amount), expected, diff --git a/odoo/addons/base/views/ir_module_views.xml b/odoo/addons/base/views/ir_module_views.xml index 249917e3701..21e69ee26e0 100644 --- a/odoo/addons/base/views/ir_module_views.xml +++ b/odoo/addons/base/views/ir_module_views.xml @@ -165,7 +165,7 @@ <div class="oe_module_vignette"> <t t-set="installed" t-value="record.state.raw_value == 'installed'"/> <div class="o_dropdown_kanban dropdown" tabindex="-1"> - <a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu"> + <a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu"> <span class="fa fa-ellipsis-v"/> </a> <div class="dropdown-menu" role="menu" aria-labelledby="dLabel"> diff --git a/odoo/addons/test_access_rights/ir.model.access.csv b/odoo/addons/test_access_rights/ir.model.access.csv index 20310a5d0da..9edd8503b72 100644 --- a/odoo/addons/test_access_rights/ir.model.access.csv +++ b/odoo/addons/test_access_rights/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_test_access_right_some_obj,access_test_access_right_some_obj,model_test_access_right_some_obj,,1,1,1,1 access_test_access_right_container,access_test_access_right_container,model_test_access_right_container,,1,1,1,1 access_test_access_right_parent,access_test_access_right_parent,model_test_access_right_parent,,1,1,1,1 +access_test_access_right_obj_categ,access_test_access_right_obj_categ,model_test_access_right_obj_categ,,1,1,1,1 diff --git a/odoo/addons/test_access_rights/models.py b/odoo/addons/test_access_rights/models.py index 4dd8c034bac..bef14bad2c2 100644 --- a/odoo/addons/test_access_rights/models.py +++ b/odoo/addons/test_access_rights/models.py @@ -8,6 +8,7 @@ class SomeObj(models.Model): _description = 'Object For Test Access Right' val = fields.Integer() + categ_id = fields.Many2one('test_access_right.obj_categ') company_id = fields.Many2one('res.company') forbidden = fields.Integer( groups='test_access_rights.test_group,!base.group_no_one,base.group_user,!base.group_public', @@ -28,3 +29,14 @@ class Parent(models.Model): _inherits = {'test_access_right.some_obj': 'obj_id'} obj_id = fields.Many2one('test_access_right.some_obj', required=True, ondelete='restrict') + +class ObjCateg(models.Model): + _name = 'test_access_right.obj_categ' + _description = "Context dependent searchable model" + + name = fields.Char(required=True) + + def search(self, args, **kwargs): + if self.env.context.get('only_media'): + args += [('name', '=', 'Media')] + return super(ObjCateg, self).search(args, **kwargs) diff --git a/odoo/addons/test_access_rights/tests/test_ir_rules.py b/odoo/addons/test_access_rights/tests/test_ir_rules.py index 9cb2f3ebda2..b10d6c22200 100644 --- a/odoo/addons/test_access_rights/tests/test_ir_rules.py +++ b/odoo/addons/test_access_rights/tests/test_ir_rules.py @@ -8,10 +8,11 @@ class TestRules(TransactionCase): def setUp(self): super(TestRules, self).setUp() - self.id1 = self.env['test_access_right.some_obj']\ - .create({'val': 1}).id - self.id2 = self.env['test_access_right.some_obj']\ - .create({'val': -1}).id + ObjCateg = self.env['test_access_right.obj_categ'] + SomeObj = self.env['test_access_right.some_obj'] + self.categ1 = ObjCateg.create({'name': 'Food'}).id + self.id1 = SomeObj.create({'val': 1, 'categ_id': self.categ1}).id + self.id2 = SomeObj.create({'val': -1, 'categ_id': self.categ1}).id # create a global rule forbidding access to records with a negative # (or zero) val self.env['ir.rule'].create({ @@ -19,6 +20,13 @@ class TestRules(TransactionCase): 'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id, 'domain_force': "[('val', '>', 0)]" }) + # create a global rule that forbid access to records without + # categories, the search is part of the test + self.env['ir.rule'].create({ + 'name': 'See all categories', + 'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id, + 'domain_force': "[('categ_id', 'in', user.env['test_access_right.obj_categ'].search([]).ids)]" + }) def test_basic_access(self): env = self.env(user=self.browse_ref('base.public_user')) @@ -82,3 +90,25 @@ class TestRules(TransactionCase): Model = env['test_access_right.some_obj'] with self.assertQueryCount(0): Model._filter_access_rules('read') + + def test_no_context_in_ir_rules(self): + """ The context should not impact the ir rules. """ + env = self.env(user=self.browse_ref('base.public_user')) + ObjCateg = self.env['test_access_right.obj_categ'] + SomeObj = self.env['test_access_right.some_obj'] + + # validate the effect of context on category search, there are + # no existing media category + self.assertTrue(ObjCateg.search([])) + self.assertFalse(ObjCateg.with_context(only_media=True).search([])) + + # record1 is food and is accessible with an empy context + ObjCateg.clear_caches() + records = SomeObj.search([('id', '=', self.id1)]) + self.assertTrue(records) + + # it should also be accessible as the context is not used when + # searching for SomeObjs + ObjCateg.clear_caches() + records = SomeObj.with_context(only_media=True).search([('id', '=', self.id1)]) + self.assertTrue(records) diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 73823258b7b..c85e87d1295 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -707,7 +707,8 @@ class ChromeBrowser(): def set_cookie(self, name, value, path, domain): params = {'name': name, 'value': value, 'path': path, 'domain': domain} - self._websocket_send('Network.setCookie', params=params) + _id = self._websocket_send('Network.setCookie', params=params) + return self._websocket_wait_id(_id) def _wait_ready(self, ready_code, timeout=60): self._logger.info('Evaluate ready code "%s"', ready_code) diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index 47b5d72c069..5dc13a7f8de 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -72,7 +72,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_ normalized_value = value / rounding_factor # normalize sign = math.copysign(1.0, normalized_value) epsilon_magnitude = math.log(abs(normalized_value), 2) - epsilon = 2**(epsilon_magnitude-53) + epsilon = 2**(epsilon_magnitude-52) # TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations) # When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value |
