summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-08-20 12:17:04 +0200
committerChristophe Simonis <chs@odoo.com>2019-08-20 12:17:04 +0200
commitf26de445e5579913e27ea6a630bcee6d40eefc7d (patch)
tree8b8e06be8a9d5fd3c1e5c31634611127446b9948 /addons
parentbc6accc5688d3c38a35949751779f808e9c2063f (diff)
parent6f55fd65da8c50d36ca97654a920fb075076f125 (diff)
[MERGE] forward port branch saas-12.3 up to 6f55fd65da8
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_move.py4
-rw-r--r--addons/account/models/account_payment.py5
-rw-r--r--addons/account/models/account_reconcile_model.py2
-rw-r--r--addons/account/views/res_config_settings_views.xml2
-rw-r--r--addons/base_iban/models/res_partner_bank.py1
-rw-r--r--addons/base_import/models/base_import.py3
-rw-r--r--addons/base_vat/models/res_partner.py15
-rw-r--r--addons/calendar/views/calendar_views.xml6
-rw-r--r--addons/crm/views/crm_lead_views.xml2
-rw-r--r--addons/event/views/event_views.xml2
-rw-r--r--addons/hr_contract/views/hr_contract_views.xml2
-rw-r--r--addons/hw_escpos/escpos/escpos.py25
-rw-r--r--addons/iap/models/iap.py6
-rw-r--r--addons/l10n_ae/data/account_tax_template_data.xml11
-rw-r--r--addons/l10n_it_edi/models/account_invoice.py6
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py2
-rw-r--r--addons/l10n_it_edi/models/res_company.py10
-rw-r--r--addons/mail/models/mail_channel.py14
-rw-r--r--addons/mail/models/mail_thread.py4
-rw-r--r--addons/mail/static/src/js/models/threads/document_thread.js1
-rw-r--r--addons/mail/static/src/js/services/mail_manager.js3
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js2
-rw-r--r--addons/mail/static/src/js/systray/systray_activity_menu.js1
-rw-r--r--addons/mail/static/src/scss/discuss.scss1
-rw-r--r--addons/mail/static/tests/chatter_tests.js65
-rw-r--r--addons/mail/static/tests/discuss_tests.js65
-rw-r--r--addons/mail/static/tests/systray/systray_activity_menu_tests.js8
-rw-r--r--addons/mail/static/tests/systray/systray_messaging_menu_tests.js57
-rw-r--r--addons/mail/wizard/mail_resend_message.py2
-rw-r--r--addons/maintenance/models/maintenance.py2
-rw-r--r--addons/maintenance/views/maintenance_views.xml4
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/mass_mailing/views/mass_mailing_views.xml6
-rw-r--r--addons/mass_mailing/views/res_config_settings_views.xml2
-rw-r--r--addons/mrp/models/mrp_workorder.py5
-rw-r--r--addons/note/views/note_views.xml2
-rw-r--r--addons/partner_autocomplete/models/res_partner.py2
-rw-r--r--addons/point_of_sale/static/src/js/screens.js5
-rw-r--r--addons/point_of_sale/wizard/pos_details.xml2
-rw-r--r--addons/portal/controllers/portal.py6
-rw-r--r--addons/pos_mercury/views/pos_config_setting_views.xml4
-rw-r--r--addons/pos_mercury/views/pos_config_views.xml7
-rw-r--r--addons/project/views/project_views.xml2
-rw-r--r--addons/purchase/models/purchase.py2
-rw-r--r--addons/purchase_stock/models/stock.py3
-rw-r--r--addons/purchase_stock/tests/test_create_picking.py62
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale_coupon/models/sale_order.py4
-rw-r--r--addons/sale_management/models/sale_order_template.py6
-rw-r--r--addons/sale_management/views/sale_order_views.xml5
-rw-r--r--addons/sale_stock/models/stock.py3
-rw-r--r--addons/sale_timesheet/models/account_move.py2
-rw-r--r--addons/sale_timesheet/models/sale_order.py3
-rw-r--r--addons/stock/models/product.py4
-rw-r--r--addons/stock/models/res_config_settings.py10
-rw-r--r--addons/stock/models/stock_production_lot.py2
-rw-r--r--addons/stock_picking_batch/report/report_picking_batch.xml2
-rw-r--r--addons/survey/views/survey_report_templates.xml7
-rw-r--r--addons/survey/views/survey_survey_views.xml2
-rw-r--r--addons/web/static/src/js/core/utils.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js6
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js44
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js14
-rw-r--r--addons/web/static/src/scss/list_view.scss3
-rw-r--r--addons/web/static/tests/views/kanban_mobile_tests.js31
-rw-r--r--addons/web/static/tests/views/list_tests.js13
-rw-r--r--addons/website/models/mixins.py7
-rw-r--r--addons/website/models/website.py13
-rw-r--r--addons/website/models/website_menu.py2
-rw-r--r--addons/website/tests/test_ui.py51
-rw-r--r--addons/website_event/static/src/js/website_geolocation.js12
-rw-r--r--addons/website_mass_mailing/views/res_config_settings_views.xml3
-rw-r--r--addons/website_payment/models/payment_acquirer.py2
-rw-r--r--addons/website_sale/models/product.py2
-rw-r--r--addons/website_sale_comparison/controllers/main.py11
-rw-r--r--addons/website_sale_wishlist/models/product_wishlist.py2
-rw-r--r--addons/website_twitter/models/website_twitter_tweet.py2
77 files changed, 584 insertions, 133 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 91a1d4cf05d..c6933190d78 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -2173,9 +2173,9 @@ class AccountMoveLine(models.Model):
help="The bank statement used for bank reconciliation")
# ==== Tax fields ====
- tax_ids = fields.Many2many('account.tax', string='Taxes')
+ tax_ids = fields.Many2many('account.tax', string='Taxes', help="Taxes that apply on the base amount")
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', store=True,
- compute='_compute_tax_line_id')
+ compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line")
tax_base_amount = fields.Monetary(string="Base Amount", store=True,
currency_field='company_currency_id')
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 7fbb328c69d..d04a4b05520 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 2834791ec38..108d7b75550 100644
--- a/addons/account/models/account_reconcile_model.py
+++ b/addons/account/models/account_reconcile_model.py
@@ -580,6 +580,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 f557e54b64d..a58c574020b 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 44b039a06f8..544adbdf4bc 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 f7227a99192..dfd6f7a8e49 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 0fc78a8138d..edcf0ddf57e 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -391,7 +391,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 dfcbcd75aa3..e3ac9ddf655 100644
--- a/addons/event/views/event_views.xml
+++ b/addons/event/views/event_views.xml
@@ -318,7 +318,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 86f4f2593da..69c6345cddf 100644
--- a/addons/hr_contract/views/hr_contract_views.xml
+++ b/addons/hr_contract/views/hr_contract_views.xml
@@ -192,7 +192,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 7dd9b6c206b..881771c7fa1 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 649b2d2c683..3a8d8361b95 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -95,7 +95,7 @@ class AccountMove(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 AccountMove(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>
@@ -730,7 +730,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 ec2a96b93e4..bafdf1cb464 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 c68f6cc3f7f..df5455df4d6 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)
@@ -426,9 +426,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 3536c4e6001..9d1bd8c4d01 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -937,7 +937,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
@@ -1365,7 +1365,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/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 4a20d24af8d..3ed4031abeb 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -85,7 +85,7 @@ var MailManager = AbstractService.extend({
addMessage: function (data, options) {
options = options || {};
var message = this.getMessage(data.id);
- var prom = Promise.resolve();
+ var prom;
if (!message) {
prom = this._addNewMessage(data, options);
} else {
@@ -95,6 +95,7 @@ var MailManager = AbstractService.extend({
});
}
this._addMessageToThreads(message, options);
+ prom = Promise.resolve(message);
}
return prom;
},
diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js
index 5cc965b5c7a..a832b0b0793 100644
--- a/addons/mail/static/src/js/services/mail_window_manager.js
+++ b/addons/mail/static/src/js/services/mail_window_manager.js
@@ -106,7 +106,7 @@ MailManager.include({
// This may happen due to concurrent calls to this method from
// messaging menu preview click and handling of longpolling chat
// window state.
- return;
+ return Promise.resolve();
}
var threadWindow = this._getThreadWindow(threadID);
var prom = Promise.resolve();
diff --git a/addons/mail/static/src/js/systray/systray_activity_menu.js b/addons/mail/static/src/js/systray/systray_activity_menu.js
index fae95a78c2f..641ff4be1bc 100644
--- a/addons/mail/static/src/js/systray/systray_activity_menu.js
+++ b/addons/mail/static/src/js/systray/systray_activity_menu.js
@@ -108,6 +108,7 @@ var ActivityMenu = Widget.extend({
*/
_onActivityActionClick: function (ev) {
ev.stopPropagation();
+ this.$('.dropdown-toggle').dropdown('toggle');
var targetAction = $(ev.currentTarget);
var actionXmlid = targetAction.data('action_xmlid');
if (actionXmlid) {
diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss
index 6092ab733fc..8284fdc70b5 100644
--- a/addons/mail/static/src/scss/discuss.scss
+++ b/addons/mail/static/src/scss/discuss.scss
@@ -110,6 +110,7 @@
}
.o_mail_thread {
+ position: relative;
flex: 1 0 0;
overflow: auto;
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index bed4d682d10..4c9d55fcb75 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -1144,6 +1144,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.nextTick();
+ 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/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 8c65266a392..226fd1e398a 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -59,6 +59,7 @@ QUnit.module('Discuss', {
type: 'integer',
},
},
+ records: [],
},
'res.partner': {
fields: {
@@ -1490,6 +1491,70 @@ QUnit.test('custom-named DM conversation', async function (assert) {
discuss.destroy();
});
+QUnit.test('receive channel message notification then delayed needaction notification', async function (assert) {
+ assert.expect(3);
+
+ const message = {
+ author_id: [5, 'Demo User'],
+ body: '<p>test</p>',
+ channel_ids: [1],
+ id: 100,
+ model: 'mail.channel',
+ needaction: true,
+ needaction_partner_ids: [3],
+ res_id: 1,
+ };
+ this.data.initMessaging = {
+ channel_slots: {
+ channel_channel: [{
+ id: 1,
+ channel_type: 'channel',
+ name: "general",
+ }],
+ },
+ };
+
+ const discuss = await createDiscuss({
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ session: {
+ partner_id: 3
+ },
+ });
+
+ const $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]');
+ assert.hasClass(
+ $inbox,
+ 'o_active',
+ "'Inbox' should be the currently active thread");
+ assert.containsNone(
+ discuss,
+ '.o_thread_message',
+ "inbox should contain no messages initially");
+
+ // simulate new needaction message posted on channnel
+ this.data['mail.message'].records.push(message);
+ // simulate receiving channel notification
+ discuss.call('bus_service', 'trigger', 'notification', [
+ [['myDB', 'mail.channel', 1], message]
+ ]);
+ // short delay after receiving needaction notification
+ await testUtils.nextTick();
+ // simulate receiving needaction message notification after a short delay
+ discuss.call('bus_service', 'trigger', 'notification', [
+ [['myDB', 'ir.needaction', 3], message]
+ ]);
+ await testUtils.nextTick();
+ assert.containsOnce(
+ discuss,
+ '.o_thread_message',
+ "inbox should contain one message");
+
+ discuss.destroy();
+});
+
QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) {
assert.expect(2);
diff --git a/addons/mail/static/tests/systray/systray_activity_menu_tests.js b/addons/mail/static/tests/systray/systray_activity_menu_tests.js
index b2f76feb832..7d24b5e05a4 100644
--- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js
@@ -153,7 +153,7 @@ QUnit.test('activity menu widget: activity menu with 3 records', async function
});
QUnit.test('activity menu widget: activity view icon', async function (assert) {
- assert.expect(10);
+ assert.expect(12);
var self = this;
var activityMenu = new ActivityMenu();
testUtils.mock.addMockEnvironment(activityMenu, {
@@ -193,9 +193,15 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) {
// click on the "Issue" activity icon
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
+ assert.hasClass(activityMenu.$('.dropdown-menu'), 'show',
+ "dropdown should be expanded");
+
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']"));
+ assert.doesNotHaveClass(activityMenu.$('.dropdown-menu'), 'show',
+ "dropdown should be collapsed");
// click on the "Note" activity icon
+ await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']"));
assert.verifySteps([
diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
index b31b12f15b3..610e689ae4a 100644
--- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
@@ -920,5 +920,62 @@ QUnit.test('messaging menu widget: expand on thread preview', async function (as
messagingMenu.destroy();
});
+QUnit.test('messaging menu widget: click twice preview on slow message_fetch should open chat window once', async function (assert) {
+ // This test assumes that a condition for opening chat window is to
+ // successfully fetch messages beforehand.
+ assert.expect(1);
+
+ const self = this;
+ // Used to pause `message_fetch` after opening the messaging menu.
+ // This is necessary `message_fetch` on mailbox_inbox is required to
+ // display the previews.
+ let lockMessageFetch = false;
+ const messageFetchProm = testUtils.makeTestPromise();
+
+ const messagingMenu = new MessagingMenu();
+ testUtils.addMockEnvironment(messagingMenu, {
+ services: this.services,
+ data: this.data,
+ session: { partner_id: 1 },
+ async mockRPC(route, args) {
+ if (args.method === 'message_fetch' && lockMessageFetch) {
+ const _super = this._super.bind(this);
+ await messageFetchProm;
+ return _super(route, args);
+ }
+ if (args.method === 'channel_minimize') {
+ // called to detach thread in chat window
+ // simulate longpolling response with new chat window state
+ const channelInfo = {
+ ...self.data['mail.channel'].records[0],
+ is_minimized: true,
+ state: 'open',
+ };
+ const notifications = [ [['myDB', 'res.partner'], channelInfo] ];
+ messagingMenu.call('bus_service', 'trigger', 'notification', notifications);
+ }
+ return this._super(...arguments);
+ },
+ });
+ await messagingMenu.appendTo($('#qunit-fixture'));
+
+ // Opening chat window 1st time from messaging menu (pending from `messageFetchDef`)
+ await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
+ lockMessageFetch = true;
+ await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
+ // Click again on preview to open chat window
+ await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
+ await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
+ messageFetchProm.resolve();
+ await testUtils.nextTick();
+
+ assert.containsOnce(
+ $,
+ '.o_thread_window',
+ "should only display a single chat window");
+
+ messagingMenu.destroy();
+});
+
});
});
diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py
index bcff9dc9840..5235220240e 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 3677527e17a..e5fddbd8550 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 332cceeb14d..14f17c71c3e 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 243fc33af0a..31aaf7844e5 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -252,7 +252,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 d590f366dec..e0692193572 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 b880b53d6bc..349e18cbcf0 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -249,6 +249,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 8395223380f..973a4ae309e 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 7cd57158f22..6b5561401ad 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 94998f98052..2a122fb8d82 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -577,7 +577,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 336ab7b94f4..fabc0dc4196 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -405,6 +405,8 @@ class PurchaseOrder(models.Model):
'default_type': 'in_invoice',
'default_purchase_id': self.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 9a1e06fe644..7678f58c321 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -199,7 +199,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 18b7369a697..4bf9cbb2b9e 100644
--- a/addons/purchase_stock/tests/test_create_picking.py
+++ b/addons/purchase_stock/tests/test_create_picking.py
@@ -314,6 +314,68 @@ class TestCreatePicking(common.TestProductCommon):
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)
+
def create_delivery_order(self, propagate_date, propagate_date_minimum_delta):
stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 6c741044e78..fef3f226c10 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1083,7 +1083,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_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py
index fc49b4abc73..4bb7ee0217a 100644
--- a/addons/sale_coupon/models/sale_order.py
+++ b/addons/sale_coupon/models/sale_order.py
@@ -305,14 +305,14 @@ class SaleOrder(models.Model):
# Check commit 6bb42904a03 for next if/else
# Remove reward line if price or qty equal to 0
if values['product_uom_qty'] and values['price_unit']:
- order.write({'order_line': [(1, line.id, values) for line in lines]})
+ lines.write(values)
else:
if program.reward_type != 'free_shipping':
# Can't remove the lines directly as we might be in a recordset loop
lines_to_remove += lines
else:
values.update(price_unit=0.0)
- order.write({'order_line': [(1, line.id, values) for line in lines]})
+ lines.write(values)
return lines_to_remove
self.ensure_one()
diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py
index 1cf3ecf6abf..c120a92cb47 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 d67671ab6de..c967616bb23 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 21972630368..f7342a5fee8 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_move.py b/addons/sale_timesheet/models/account_move.py
index e43ea256daa..eee162b7bae 100644
--- a/addons/sale_timesheet/models/account_move.py
+++ b/addons/sale_timesheet/models/account_move.py
@@ -63,7 +63,7 @@ class AccountMoveLine(models.Model):
sale_line_delivery = 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': line.move_id.id,
})
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index dd903ba7330..973bea31124 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/product.py b/addons/stock/models/product.py
index 7b0ae24677c..6ad314d0e5c 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -751,7 +751,9 @@ class UoM(models.Model):
procurement_uom = self
computed_qty = qty
get_param = self.env['ir.config_parameter'].sudo().get_param
- if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
+ if get_param('stock.propagate_uom') != '1':
computed_qty = self._compute_quantity(qty, quant_uom, rounding_method='HALF-UP')
procurement_uom = quant_uom
+ else:
+ computed_qty = self._compute_quantity(qty, procurement_uom, rounding_method='HALF-UP')
return (computed_qty, procurement_uom)
diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py
index df325822e27..c52262e9515 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):
@@ -54,13 +53,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 433b5c67e47..1a9f94e1e28 100644
--- a/addons/stock/models/stock_production_lot.py
+++ b/addons/stock/models/stock_production_lot.py
@@ -54,7 +54,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_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&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;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_report_templates.xml b/addons/survey/views/survey_report_templates.xml
index 55b209079f7..df969747027 100644
--- a/addons/survey/views/survey_report_templates.xml
+++ b/addons/survey/views/survey_report_templates.xml
@@ -15,12 +15,15 @@
<h2>
<span t-esc="user_input.partner_id.name or user_input.email"/>
</h2>
- <p>Successfully achieved</p>
+ <!-- You should not print a certificate if failed :) -->
+ <p t-if="user_input.quizz_passed">Successfully achieved</p>
+ <p t-if="not user_input.quizz_passed">Successfully failed</p>
<h3>
<span t-field="user_input.survey_id.display_name"/>
</h3>
<p>
- <strong>Date of Certification:</strong>
+ <strong t-if="user_input.quizz_passed">Date of Certification:</strong>
+ <strong t-if="not user_input.quizz_passed">Date of Failure:</strong>
<span t-field="user_input.create_date"/>
</p>
</div>
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
index 3319f518623..0c2b219c411 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -146,7 +146,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 44e17324b85..622050c9626 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -522,7 +522,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 d3165420ef8..6d1b31ea8ea 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/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 3c36720081e..f87268ddb1e 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -502,16 +502,16 @@ ListRenderer.include({
switch (field.type) {
case 'binary': return 1;
case 'boolean': return '40px';
- case 'char': return 1;
- case 'date': return '100px';
- case 'datetime': return '150px';
- case 'float': return '100px';
+ case 'char': return 2;
+ case 'date': return '92px';
+ case 'datetime': return '146px';
+ case 'float': return '92px';
case 'html': return 3;
- case 'integer': return '80px';
+ case 'integer': return '74px';
case 'many2many': return 2;
case 'many2one': return 2;
- case 'monetary': return 1.2;
- case 'one2many': return 2.5;
+ case 'monetary': return '104px';
+ case 'one2many': return 2;
case 'reference': return 1.5;
case 'selection': return 1.5;
case 'text': return 2.5;
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 7c766a1cc81..1c148604bc7 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -38,6 +38,9 @@
&.o_list_many2one {
white-space: pre-wrap;
}
+ &.o_list_text {
+ word-break: break-word;
+ }
}
tfoot {
diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js
index 9a9cd3cf15f..184ac985e80 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/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index f9e6b778d42..2677fb13216 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -1432,10 +1432,10 @@ QUnit.module('Views', {
QUnit.test('width of some of the fields should be hardcoded', async function (assert) {
const assertions = [
{ field: 'bar', expected: 40, type: 'Boolean' },
- { field: 'int_field', expected: 80, type: 'Integer' },
- { field: 'qux', expected: 100, type: 'Float' },
- { field: 'date', expected: 100, type: 'Date' },
- { field: 'datetime', expected: 150, type: 'Datetime' },
+ { field: 'int_field', expected: 74, type: 'Integer' },
+ { field: 'qux', expected: 92, type: 'Float' },
+ { field: 'date', expected: 92, type: 'Date' },
+ { field: 'datetime', expected: 146, type: 'Datetime' },
];
assert.expect(assertions.length + 1);
@@ -1497,9 +1497,8 @@ QUnit.module('Views', {
});
// the width is hardcoded to make sure we have the same condition
- // between debug mode and non debug mode (#qunit-fixture is limited to
- // 1000px)
- list.$el.width('1000px');
+ // between debug mode and non debug mode
+ list.$el.width('1200px');
var startHeight = list.$('.o_data_row:first').height();
// start edition of first row
diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py
index 39a14b6060e..11767887e40 100644
--- a/addons/website/models/mixins.py
+++ b/addons/website/models/mixins.py
@@ -103,7 +103,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 3daec56e121..2e10d7d1cc4 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -170,6 +170,19 @@ class Website(models.Model):
if 'favicon' in vals:
vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
+ @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 0660d232752..2db952cc62c 100644
--- a/addons/website/tests/test_ui.py
+++ b/addons/website/tests/test_ui.py
@@ -6,6 +6,57 @@ 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': '',
+ '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 &amp; 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 a0909186eec..1109d11d8d2 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -29,7 +29,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')