summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-08-21 10:10:11 +0200
committerChristophe Simonis <chs@odoo.com>2019-08-21 10:10:11 +0200
commit3faea8fbf810224ad4e03cfa1dc36f186800c501 (patch)
tree5e05c40d62dacc5951c70b3686f30a7bbb86f399 /addons
parentcc2d78beeaf61fa0b8c9543196d1b4fce5cd5da6 (diff)
parentf26de445e5579913e27ea6a630bcee6d40eefc7d (diff)
[MERGE] forward port branch saas-12.4 up to f26de445e55
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/models/hr_employee_base.py3
-rw-r--r--addons/hr/models/hr_employee_public.py1
-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/mailing_contact.py2
-rw-r--r--addons/mass_mailing/views/mailing_list_views.xml2
-rw-r--r--addons/mass_mailing/views/mailing_mailing_views.xml2
-rw-r--r--addons/mass_mailing_sale/models/mailing_mailing.py19
-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.py4
-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.py12
-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_payment/models/payment_acquirer.py8
-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
79 files changed, 593 insertions, 143 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index a7abbf8ab82..647043e8d81 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -2146,9 +2146,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_group_id = fields.Many2one(related='tax_line_id.tax_group_id', string='Originator tax group',
readonly=True, store=True,
help='technical field for widget tax-group-custom-field')
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 15cc5bf499e..de720f64d51 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -450,7 +450,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,
+ })
def unlink(self):
if any(bool(rec.move_line_ids) for rec in self):
diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py
index f3b9973adc6..1b68ed9cac1 100644
--- a/addons/account/models/account_reconcile_model.py
+++ b/addons/account/models/account_reconcile_model.py
@@ -590,6 +590,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 e6410dfcc68..411696d151f 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 c750af9aae7..2c9f4f868b9 100644
--- a/addons/base_iban/models/res_partner_bank.py
+++ b/addons/base_iban/models/res_partner_bank.py
@@ -168,6 +168,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 76ad4683e28..a9cc382871e 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -442,6 +442,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 983f616f1f3..e06f1ac8f02 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
@@ -64,6 +71,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',
@@ -399,6 +408,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
+
# Argentinian VAT validation, contributed by ADHOC
def check_vat_ar(self, vat):
try:
diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml
index 01c3b571cd2..aae443953ee 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 135fa611402..2a14dc42b87 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -466,7 +466,7 @@
<div t-attf-class="#{!selection_mode ? 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 4307550b2b9..aaf2df32a89 100644
--- a/addons/event/views/event_views.xml
+++ b/addons/event/views/event_views.xml
@@ -318,7 +318,7 @@
<div t-attf-class="{{!selection_mode ? '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/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py
index c86a1411e23..4548cb4382f 100644
--- a/addons/hr/models/hr_employee_base.py
+++ b/addons/hr/models/hr_employee_base.py
@@ -26,6 +26,9 @@ class HrEmployeeBase(models.AbstractModel):
user_id = fields.Many2one('res.users')
resource_id = fields.Many2one('resource.resource')
resource_calendar_id = fields.Many2one('resource.calendar', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ tz = fields.Selection(
+ string='Timezone', related='resource_id.tz', readonly=False,
+ help="This field is used in order to define in which timezone the resources will work.")
hr_presence_state = fields.Selection([
('present', 'Present'),
('absent', 'Absent'),
diff --git a/addons/hr/models/hr_employee_public.py b/addons/hr/models/hr_employee_public.py
index 62c221ee472..3d1fe02cc4f 100644
--- a/addons/hr/models/hr_employee_public.py
+++ b/addons/hr/models/hr_employee_public.py
@@ -28,6 +28,7 @@ class HrEmployeePublic(models.Model):
user_id = fields.Many2one(readonly=True)
resource_id = fields.Many2one(readonly=True)
resource_calendar_id = fields.Many2one(readonly=True)
+ tz = fields.Selection(readonly=True)
color = fields.Integer(readonly=True)
# hr.employee.public specific fields
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml
index 5ab887b44d0..ea4b5bf6cef 100644
--- a/addons/hr_contract/views/hr_contract_views.xml
+++ b/addons/hr_contract/views/hr_contract_views.xml
@@ -242,7 +242,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown" t-if="!selection_mode" 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 b90bfff5ccb..608aa52c788 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 ed6cf716493..2938c6630ff 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -94,7 +94,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:
@@ -109,7 +109,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>
@@ -737,6 +737,6 @@ 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 176b89f68d7..7b030b32234 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."))
def fetch_mail(self):
""" WARNING: meant for cron usage only - will commit() after each email! """
diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py
index e37b451d36b..71c651f84e9 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 e94174b7591..91d40bed9de 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -139,23 +139,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.'))
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
@@ -239,7 +239,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."))
result = super(Channel, self).write(vals)
@@ -400,9 +400,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!"))
def _send_guidelines(self, partners):
""" Send guidelines of a given channel. Returns False if template used for guidelines
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 2016f07e0c3..80b4db5131b 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -878,7 +878,7 @@ class MailThread(models.AbstractModel):
# compute references to find if message is a reply to an existing thread
thread_references = message_dict['references'] or message_dict['in_reply_to']
msg_references = [ref for ref in tools.mail_header_msgid_re.findall(thread_references) if 'reply_to' not in ref]
- mail_messages = self.env['mail.message'].sudo().search([('message_id', 'in', msg_references)], limit=1)
+ mail_messages = self.env['mail.message'].sudo().search([('message_id', 'in', msg_references)], limit=1, order='id desc, message_id')
is_a_reply = bool(mail_messages)
reply_model, reply_thread_id = mail_messages.model, mail_messages.res_id
@@ -1366,7 +1366,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_message_header(message, '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 2aa083a56d2..27208f0edfc 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 1f84918f36d..f624e5ae308 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 f777efd785e..b56d7747e8e 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 f0f3394c1e1..fd59686aeb5 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -1233,6 +1233,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 2cad78eed8b..9949af7c28c 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -64,6 +64,7 @@ QUnit.module('Discuss', {
type: 'integer',
},
},
+ records: [],
},
'res.partner': {
fields: {
@@ -1517,6 +1518,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);
// Promise to simulate late server response on message post
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 cc5b099a949..a256267fd9c 100644
--- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js
@@ -156,7 +156,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, {
@@ -197,9 +197,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 dc659681dac..e68144af84d 100644
--- a/addons/mail/wizard/mail_resend_message.py
+++ b/addons/mail/wizard/mail_resend_message.py
@@ -46,7 +46,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
def resend_mail_action(self):
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index c6d5da099d3..2ab6df51909 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -46,7 +46,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)
def _compute_equipment_count(self):
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index 48b5d767780..9d1c2c06ebc 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -137,7 +137,7 @@
<div t-attf-class="{{!selection_mode ? '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">
@@ -438,7 +438,7 @@
<div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown" t-if="!selection_mode">
- <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/mailing_contact.py b/addons/mass_mailing/models/mailing_contact.py
index 5c0e6a3a860..e50b36534e4 100644
--- a/addons/mass_mailing/models/mailing_contact.py
+++ b/addons/mass_mailing/models/mailing_contact.py
@@ -78,7 +78,7 @@ class MassMailingContact(models.Model):
contacts = self.env['mailing.contact.subscription'].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/mailing_list_views.xml b/addons/mass_mailing/views/mailing_list_views.xml
index 6ee9811931f..4f57c5d8501 100644
--- a/addons/mass_mailing/views/mailing_list_views.xml
+++ b/addons/mass_mailing/views/mailing_list_views.xml
@@ -114,7 +114,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" 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/mailing_mailing_views.xml b/addons/mass_mailing/views/mailing_mailing_views.xml
index fe5b5ad2f1b..d6672f4bab2 100644
--- a/addons/mass_mailing/views/mailing_mailing_views.xml
+++ b/addons/mass_mailing/views/mailing_mailing_views.xml
@@ -236,7 +236,7 @@
<t t-name="kanban-box">
<div t-attf-class="{{!selection_mode ? '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" t-if="!selection_mode">
- <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_sale/models/mailing_mailing.py b/addons/mass_mailing_sale/models/mailing_mailing.py
index 08716037a55..4d4081da489 100644
--- a/addons/mass_mailing_sale/models/mailing_mailing.py
+++ b/addons/mass_mailing_sale/models/mailing_mailing.py
@@ -20,16 +20,11 @@ class MassMailing(models.Model):
@api.depends('mailing_domain')
def _compute_sale_invoiced_amount(self):
- has_so_access = self.env['sale.order'].check_access_rights('read', raise_exception=False)
- has_invoice_report_access = self.env['account.invoice.report'].check_access_rights('read', raise_exception=False)
for mass_mailing in self:
- if has_so_access and has_invoice_report_access:
- invoices = self.env['sale.order'].search(self._get_sale_utm_domain()).mapped('invoice_ids')
- res = self.env['account.invoice.report'].search_read(
- [('move_id', 'in', invoices.ids), ('state', 'not in', ['draft', 'cancel'])],
- ['price_subtotal']
- )
- mass_mailing.sale_invoiced_amount = sum(r['price_subtotal'] for r in res)
+ if self.user_has_groups('sales_team.group_sale_salesman') and self.user_has_groups('account.group_account_invoice'):
+ domain = self._get_sale_utm_domain() + [('state', 'not in', ['draft', 'cancel'])]
+ moves = self.env['account.move'].search_read(domain, ['amount_untaxed'])
+ mass_mailing.sale_invoiced_amount = sum(i['amount_untaxed'] for i in moves)
else:
mass_mailing.sale_invoiced_amount = 0
@@ -40,15 +35,15 @@ class MassMailing(models.Model):
return action
def action_redirect_to_invoiced(self):
- action = self.env.ref('account.action_move_journal_line').read()[0]
- invoices = self.env['sale.order'].search(self._get_sale_utm_domain()).mapped('invoice_ids')
+ action = self.env.ref('account.action_move_out_invoice_type').read()[0]
+ moves = self.env['account.move'].search(self._get_sale_utm_domain())
action['context'] = {
'create': False,
'edit': False,
'view_no_maturity': True
}
action['domain'] = [
- ('id', 'in', invoices.ids),
+ ('id', 'in', moves.ids),
('type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt')),
('state', 'not in', ['draft', 'cancel'])
]
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 1766293e0e1..6fe33d5170e 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -259,6 +259,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)
+
def write(self, values):
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
raise UserError(_('You can not change the finished work order.'))
diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml
index 051b7abe0e2..cb1ba5cb4ae 100644
--- a/addons/note/views/note_views.xml
+++ b/addons/note/views/note_views.xml
@@ -97,7 +97,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 4a38ffb7855..1821dfd20f1 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 d5d149a02e4..3c1747a1d01 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 4425266df38..0b6ea4c7983 100644
--- a/addons/portal/controllers/portal.py
+++ b/addons/portal/controllers/portal.py
@@ -323,7 +323,7 @@ class CustomerPortal(Controller):
document = request.env[model_name].browse([document_id])
document_sudo = document.with_user(SUPERUSER_ID).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')
@@ -359,12 +359,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 3c92ea27f6b..e7f09273aae 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -574,7 +574,7 @@
</small>
</div>
<div class="o_dropdown_kanban dropdown" t-if="!selection_mode" 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 a00aa4e0606..3f5beb0ff1f 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -393,6 +393,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 b54abba7fd2..3598a06645d 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -196,7 +196,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 a727fea2371..de209d06319 100644
--- a/addons/purchase_stock/tests/test_create_picking.py
+++ b/addons/purchase_stock/tests/test_create_picking.py
@@ -315,6 +315,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 0e93cf69cec..cb293cd5d97 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1073,7 +1073,7 @@ class SaleOrderLine(models.Model):
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 636f5e65281..e1249c86abc 100644
--- a/addons/sale_coupon/models/sale_order.py
+++ b/addons/sale_coupon/models/sale_order.py
@@ -303,14 +303,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 745ac2b3ff4..71ca1f9acdd 100644
--- a/addons/sale_management/models/sale_order_template.py
+++ b/addons/sale_management/models/sale_order_template.py
@@ -50,7 +50,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)
@@ -99,7 +99,7 @@ class SaleOrderTemplateLine(models.Model):
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 feb8162110b..54b8bc35716 100644
--- a/addons/sale_management/views/sale_order_views.xml
+++ b/addons/sale_management/views/sale_order_views.xml
@@ -103,12 +103,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 325650aedd9..3e4eb8c9180 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 5ef655898be..7e316a523e4 100644
--- a/addons/sale_timesheet/models/account_move.py
+++ b/addons/sale_timesheet/models/account_move.py
@@ -61,7 +61,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 9ca508870da..4f06e298a00 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -85,7 +85,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 6571459ee20..56844ddb5b1 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -830,7 +830,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 9dda679f4e6..a6675a97c0e 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):
@@ -55,13 +54,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 1307272edda..82c50565c39 100644
--- a/addons/stock/models/stock_production_lot.py
+++ b/addons/stock/models/stock_production_lot.py
@@ -53,7 +53,7 @@ class ProductionLot(models.Model):
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 4502df325f0..c1c1df41ccb 100644
--- a/addons/stock_picking_batch/report/report_picking_batch.xml
+++ b/addons/stock_picking_batch/report/report_picking_batch.xml
@@ -31,7 +31,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&amp;quiet=%s' % ('Code128', pick.name, 200, 50, 0)" alt="Barcode"/>
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;quiet=%s' % ('Code128', pick.name, 400, 100, 0)" style="width:200px;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 c41aa840223..99f46580fc1 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -159,7 +159,7 @@
<div t-name="kanban-box" t-attf-class="{{!selection_mode ? '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 and !selection_mode">
- <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 945b63e9a37..29c2ff04c40 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -534,7 +534,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 13ea270870f..289e8a3a0c2 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1434,9 +1434,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];
@@ -1451,7 +1451,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 df0ad782b60..e4dd682ed51 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
@@ -516,16 +516,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 6932628b9e9..f2fc83902a4 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -37,6 +37,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 d74bda7578b..f4eb264241d 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 b6d8887cfe3..1dad8168161 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.",
+ )
def can_access_from_current_website(self, website_id=False):
can_access = True
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index a81721d9f10..8ce3d46568a 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -174,6 +174,18 @@ class Website(models.Model):
if 'favicon' in vals:
vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
+ 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 274cb2bb5a3..e710979015f 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_payment/models/payment_acquirer.py b/addons/website_payment/models/payment_acquirer.py
index 6702aad9115..5f7c31542a4 100644
--- a/addons/website_payment/models/payment_acquirer.py
+++ b/addons/website_payment/models/payment_acquirer.py
@@ -4,6 +4,10 @@ from odoo import fields, models
class PaymentAcquirer(models.Model):
- _inherit = 'payment.acquirer'
+ _inherit = "payment.acquirer"
- website_id = fields.Many2one('website', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ website_id = fields.Many2one(
+ "website",
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
+ ondelete="restrict",
+ )
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index 630b3d44146..d3f80b639d4 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 09a5984d64a..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.valid_product_template_attribute_line_ids:
+ 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 3530168efa8..4914aae6ea2 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(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')