diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index c8d98b58f8..c41a8fcb9f 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -294,7 +294,7 @@ class AccountMove(models.Model):
default=_get_default_invoice_incoterm,
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
display_qr_code = fields.Boolean(string="Display QR-code", related='company_id.qr_code')
- qr_code_method = fields.Selection(string="Payment QR-code",
+ qr_code_method = fields.Selection(string="Payment QR-code", copy=False,
selection=lambda self: self.env['res.partner.bank'].get_available_qr_methods_in_sequence(),
help="Type of QR-code to be generated for the payment of this invoice, when printing it. If left blank, the first available and usable method will be used.")
diff --git a/addons/account_qr_code_sepa/tests/test_sepa_qr.py b/addons/account_qr_code_sepa/tests/test_sepa_qr.py
index 775414ab46..70c820c74c 100644
--- a/addons/account_qr_code_sepa/tests/test_sepa_qr.py
+++ b/addons/account_qr_code_sepa/tests/test_sepa_qr.py
@@ -3,7 +3,7 @@
from flectra.exceptions import UserError
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
from flectra.tests import tagged
-
+from flectra import fields
@tagged('post_install', '-at_install')
class TestSEPAQRCode(AccountTestInvoicingCommon):
@@ -62,3 +62,16 @@ class TestSEPAQRCode(AccountTestInvoicingCommon):
"""
self.sepa_qr_invoice.generate_qr_code()
self.assertEqual(self.sepa_qr_invoice.qr_code_method, 'sct_qr', "SEPA QR-code generator should have been chosen for this invoice.")
+
+ def test_out_invoice_create_refund_qr_code(self):
+ self.sepa_qr_invoice.generate_qr_code()
+ self.sepa_qr_invoice.action_post()
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.sepa_qr_invoice.ids).create({
+ 'date': fields.Date.from_string('2019-02-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'refund',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertFalse(reverse_move.qr_code_method, "qr_code_method for credit note should be None")
diff --git a/addons/l10n_be/data/account_data.xml b/addons/l10n_be/data/account_data.xml
index 0ac6aaf6a9..12a8df1e68 100644
--- a/addons/l10n_be/data/account_data.xml
+++ b/addons/l10n_be/data/account_data.xml
@@ -1,24 +1,24 @@
-
-
-
-
- TVA 21%
-
+
-
- TVA 12%
-
+
+
+ TVA 21%
+
-
- TVA 6%
-
+
+ TVA 12%
+
-
- TVA 0%
-
+
+ TVA 6%
+
-
-
\ No newline at end of file
+
+ TVA 0%
+
+
+
+
diff --git a/addons/l10n_it_edi/data/account_invoice_demo.xml b/addons/l10n_it_edi/data/account_invoice_demo.xml
index e7dfcbab7b..f1ec3dbef2 100644
--- a/addons/l10n_it_edi/data/account_invoice_demo.xml
+++ b/addons/l10n_it_edi/data/account_invoice_demo.xml
@@ -12,6 +12,10 @@
12345
+
+ 0803HR0
+
+
Palazzo dell'Arte
IT00000010215
diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml
index 9c25e86b72..b64d35ed61 100644
--- a/addons/l10n_it_edi/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi/data/invoice_it_template.xml
@@ -3,6 +3,12 @@
+
+
+
+
+
+
@@ -19,12 +25,12 @@
-
+
-
+
@@ -158,8 +164,14 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index 0e8e7a3284..2c80d74c4c 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -212,14 +212,14 @@ class AccountEdiFormat(models.Model):
def _l10n_it_document_type_mapping(self):
return {
- 'TD01': dict(move_type='out_invoice', import_type='in_invoice'),
- 'TD04': dict(move_type='out_refund', import_type='in_refund'),
- 'TD07': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
- 'TD08': dict(move_type='out_refund', import_type='in_refund', simplified=True),
- 'TD09': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
- 'TD17': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="service"),
- 'TD18': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="consu", partner_in_eu=True),
- 'TD19': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="consu", goods_in_italy=True),
+ 'TD01': dict(move_types=['out_invoice'], import_type='in_invoice'),
+ 'TD04': dict(move_types=['out_refund'], import_type='in_refund'),
+ 'TD07': dict(move_types=['out_invoice'], import_type='in_invoice', simplified=True),
+ 'TD08': dict(move_types=['out_refund'], import_type='in_refund', simplified=True),
+ 'TD09': dict(move_types=['out_invoice'], import_type='in_invoice', simplified=True),
+ 'TD17': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="service"),
+ 'TD18': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="consu", partner_in_eu=True),
+ 'TD19': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="consu", goods_in_italy=True),
}
def _l10n_it_get_document_type(self, invoice):
@@ -232,7 +232,7 @@ class AccountEdiFormat(models.Model):
info_services_or_goods = infos.get('services_or_goods', "both")
info_partner_in_eu = infos.get('partner_in_eu', False)
if all([
- invoice.move_type == infos.get('move_type', False),
+ invoice.move_type in infos.get('move_types', False),
is_self_invoice == infos.get('self_invoice', False),
is_simplified == infos.get('simplified', False),
info_services_or_goods in ("both", services_or_goods),
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index 8d09c794d6..63102309f1 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -183,11 +183,16 @@ class AccountMove(models.Model):
or (partner.country_id.code == 'IT' and '0000000')
or 'XXXXXXX')
+ # Represent if the document is a reverse charge refund in a single variable
+ rc_refund = self.move_type == 'in_refund' and document_type in ['TD16', 'TD17', 'TD18']
+
# Self-invoices are technically -100%/+100% repartitioned
# but functionally need to be exported as 100%
document_total = self.amount_total
if is_self_invoice:
- document_total += sum([v['tax_amount_currency'] for k, v in tax_details['tax_details'].items()])
+ document_total += sum([abs(v['tax_amount_currency']) for k, v in tax_details['tax_details'].items()])
+ if rc_refund:
+ document_total = -abs(document_total)
# Create file content.
template_values = {
@@ -225,7 +230,8 @@ class AccountMove(models.Model):
'normalize_codice_fiscale': partner._l10n_it_normalize_codice_fiscale,
'get_vat_number': get_vat_number,
'get_vat_country': get_vat_country,
- 'in_eu': in_eu
+ 'in_eu': in_eu,
+ 'rc_refund': rc_refund,
}
return template_values
diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py
index e7b80924ec..59077ff7ba 100644
--- a/addons/l10n_it_edi/models/res_partner.py
+++ b/addons/l10n_it_edi/models/res_partner.py
@@ -1,5 +1,6 @@
# -*- coding:utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
+from stdnum.it import codicefiscale, iva
from flectra import api, fields, models, _
from flectra.exceptions import UserError, ValidationError
@@ -45,7 +46,6 @@ class ResPartner(models.Model):
@api.constrains('l10n_it_codice_fiscale')
def validate_codice_fiscale(self):
- p = re.compile(r'^([A-Za-z]{6}[0-9]{2}[A-Za-z]{1}[0-9]{2}[A-Za-z]{1}[0-9]{3}[A-Za-z]{1}$)|([0-9]{11})|(IT[0-9]{11})$')
for record in self:
- if record.l10n_it_codice_fiscale and not p.match(record.l10n_it_codice_fiscale):
- raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345678901' or 'IT12345678901' for businesses.", record.l10n_it_codice_fiscale))
+ if record.l10n_it_codice_fiscale and (not codicefiscale.is_valid(record.l10n_it_codice_fiscale) and not iva.is_valid(record.l10n_it_codice_fiscale)):
+ raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345670546' or 'IT12345670546' for businesses.", record.l10n_it_codice_fiscale))
diff --git a/addons/l10n_it_edi/tests/__init__.py b/addons/l10n_it_edi/tests/__init__.py
index b10a3c47e0..80d40a7a91 100644
--- a/addons/l10n_it_edi/tests/__init__.py
+++ b/addons/l10n_it_edi/tests/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_ir_mail_server
+from . import test_res_partner
diff --git a/addons/l10n_it_edi/tests/test_res_partner.py b/addons/l10n_it_edi/tests/test_res_partner.py
new file mode 100644
index 0000000000..500b8a973c
--- /dev/null
+++ b/addons/l10n_it_edi/tests/test_res_partner.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
+from flectra.exceptions import UserError
+from flectra.tests.common import TransactionCase, tagged
+
+
+@tagged('post_install_l10n', 'post_install', '-at_install')
+class TestResPartner(TransactionCase):
+
+ def test_validate_fiscal_code(self):
+ valid_codes = [
+ "AORTHV05P30V295L",
+ "SPDTHB43S93F42VH",
+ "MDRTUV99H14X2MNU",
+ "XPTDRX73R64YPLUD",
+ "LOLXDR40T3MZRTSV",
+ "GJTIUG55DLQZRTSS",
+ "CDEOTG5PBLQZRTSE",
+ "PERTLELPALQZRTSN",
+ "IT12345678887",
+ "IT12345670546",
+ "IT95286931217",
+ "IT95867361206",
+ "IT94567689990",
+ "12345670546",
+ "95286931217",
+ "95867361206",
+ "94567689990",
+ ]
+
+ invalid_codes = [
+ "AORTHV05P34V295U",
+ "SPDTHB43O93F42VH",
+ "MDRTUVV9H14X2MNU",
+ "XPTDRX73RS4YPLUD",
+ "LOLXDRQ0T3QZRTSJ",
+ "GJTIUGR5DLQZRTSS",
+ "CDEOTG5PBLQZRTSS",
+ "PERTLEZPALQZRTSN",
+ "IT12345678901",
+ "IT12345678885",
+ "IT45689349992",
+ "IT78239131204",
+ "IT45692151219",
+ "12345678901",
+ "12345678885",
+ "45689349992",
+ "78239131204",
+ "45692151219",
+ ]
+
+ partners = self.env['res.partner']
+
+ for i, code in enumerate(invalid_codes):
+ with self.assertRaises(UserError):
+ partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
+
+ for i, code in enumerate(valid_codes):
+ partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
+
+ self.assertEqual(len(partners), len(valid_codes))
diff --git a/addons/l10n_it_edi_sdicoop/tests/test_edi_reverse_charge_xml.py b/addons/l10n_it_edi_sdicoop/tests/test_edi_reverse_charge_xml.py
index e1c6991fad..633e34a73e 100644
--- a/addons/l10n_it_edi_sdicoop/tests/test_edi_reverse_charge_xml.py
+++ b/addons/l10n_it_edi_sdicoop/tests/test_edi_reverse_charge_xml.py
@@ -55,13 +55,13 @@ class TestItEdiReverseCharge(TestItEdi):
'amount_type': 'percent',
'type_tax_use': 'purchase',
'invoice_repartition_line_ids': repartition_lines(
- RepartitionLine(100, 'base', ('+03',)),
- RepartitionLine(100, 'tax', ('+vj9',)),
- RepartitionLine(-100, 'tax', False)),
+ RepartitionLine(100, 'base', ('+03', '+vj9')),
+ RepartitionLine(100, 'tax', ('+5v',)),
+ RepartitionLine(-100, 'tax', ('-4v',))),
'refund_repartition_line_ids': repartition_lines(
- RepartitionLine(100, 'base', False),
+ RepartitionLine(100, 'base', ('-03', '-vj9')),
RepartitionLine(100, 'tax', False),
- RepartitionLine(-100, 'tax', False))
+ RepartitionLine(-100, 'tax', False)),
}
# Purchase tax 4% with Reverse Charge
cls.purchase_tax_4p = cls.env['account.tax'].with_company(cls.company).create(tax_data)
@@ -74,10 +74,15 @@ class TestItEdiReverseCharge(TestItEdi):
**tax_data,
'name': 'Tax 4% purchase Reverse Charge, in Italy',
'invoice_repartition_line_ids': repartition_lines(
- RepartitionLine(100, 'base', ('+03',)),
- RepartitionLine(100, 'tax', ('+vj3',)),
+ RepartitionLine(100, 'base', ('+03', '+vj3')),
+ RepartitionLine(100, 'tax', ('+5v',)),
+ RepartitionLine(-100, 'tax', ('-4v',))),
+ 'refund_repartition_line_ids': repartition_lines(
+ RepartitionLine(100, 'base', ('-03', '-vj3')),
+ RepartitionLine(100, 'tax', False),
RepartitionLine(-100, 'tax', False)),
}
+
cls.purchase_tax_4p_already_in_italy = cls.env['account.tax'].with_company(cls.company).create(tax_data_4p_already_in_italy)
cls.line_tax_4p_already_in_italy = cls.standard_line.copy()
cls.line_tax_4p_already_in_italy['tax_ids'] = [(6, 0, cls.purchase_tax_4p_already_in_italy.ids)]
@@ -151,11 +156,15 @@ class TestItEdiReverseCharge(TestItEdi):
),
}
cls.reverse_charge_bill_2 = cls.env['account.move'].with_company(cls.company).create(bill_data_2)
+ cls.reverse_charge_refund = cls.reverse_charge_bill.with_company(cls.company)._reverse_moves([{
+ 'invoice_date': fields.Date.from_string('2022-03-24'),
+ }])
# Posting moves -----------
cls.reverse_charge_invoice._post()
cls.reverse_charge_bill._post()
cls.reverse_charge_bill_2._post()
+ cls.reverse_charge_refund._post()
def _cleanup_etree(self, content, xpaths=None):
xpaths = {
@@ -193,3 +202,26 @@ class TestItEdiReverseCharge(TestItEdi):
"(//DettaglioLinee/Descrizione)[2]": "",
}
)
+
+ def test_reverse_charge_refund(self):
+ self._test_invoice_with_sample_file(
+ self.reverse_charge_refund,
+ "reverse_charge_bill.xml",
+ xpaths_result={
+ "//DatiGeneraliDocumento/Numero": "",
+ "//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "",
+ },
+ xpaths_file={
+ "//DatiGeneraliDocumento/Numero": "",
+ "//DatiGeneraliDocumento/ImportoTotaleDocumento": "-1808.91",
+ "//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "",
+ "(//DettaglioLinee/PrezzoUnitario)[1]": "-800.400000",
+ "(//DettaglioLinee/PrezzoUnitario)[2]": "-800.400000",
+ "(//DettaglioLinee/PrezzoTotale)[1]": "-800.40",
+ "(//DettaglioLinee/PrezzoTotale)[2]": "-800.40",
+ "(//DatiRiepilogo/ImponibileImporto)[1]": "-800.40",
+ "(//DatiRiepilogo/ImponibileImporto)[2]": "-800.40",
+ "(//DatiRiepilogo/Imposta)[1]": "-176.09",
+ "(//DatiRiepilogo/Imposta)[2]": "-32.02",
+ }
+ )
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js
index 12f9a0204d..840c17c06e 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js
@@ -347,7 +347,7 @@ var MassMailingFieldHtml = FieldHtml.extend({
var $snippets_menu = $snippetsSideBar.find("#snippets_menu");
for (const button of $snippets_menu.get(0).children) {
- if (button.textContent === 'Select a template') {
+ if (!button.hasAttribute('tabindex') && !button.hasAttribute('accesskey')) {
button.style.display = 'none';
}
}
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index a46c76be27..eb3768cb1a 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -531,7 +531,7 @@
[('picking_type_id', '=', active_id)]
- {}
+ {'default_picking_type_id': active_id}
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 2aa3d19f4c..e8a9dc971b 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -2268,6 +2268,16 @@ exports.Orderline = Backbone.Model.extend({
return this.get_base_price();
}
},
+ get_taxed_lst_unit_price: function(){
+ var lst_price = this.get_lst_price();
+ if (this.pos.config.iface_tax_included === 'total') {
+ var product = this.get_product();
+ var taxes_ids = product.taxes_id;
+ var product_taxes = this.get_taxes_after_fp(taxes_ids);
+ return this.compute_all(product_taxes, lst_price, 1, this.pos.currency.rounding).total_included;
+ }
+ return lst_price;
+ },
get_price_without_tax: function(){
return this.get_all_prices().priceWithoutTax;
},
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
index e4ede63651..1ba2d70431 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
@@ -37,9 +37,9 @@
at
+ props.line.get_unit_display_price() < props.line.get_taxed_lst_unit_price()">
-
+
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index 4412f17fc2..3d99b1ea78 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -335,7 +335,8 @@ class PurchaseOrderLine(models.Model):
result = super(PurchaseOrderLine, self).write(values)
if 'price_unit' in values:
for line in lines:
- moves = line.move_ids.filtered(lambda s: s.state not in ('cancel', 'done'))
+ # Avoid updating kit components' stock.move
+ moves = line.move_ids.filtered(lambda s: s.state not in ('cancel', 'done') and s.product_id == line.product_id)
moves.write({'price_unit': line._get_stock_move_price_unit()})
if 'product_qty' in values:
lines.with_context(previous_product_qty=previous_product_qty)._create_or_update_picking()
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index dbee2ec9b6..5afb6580fa 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1188,6 +1188,7 @@ class SaleOrderLine(models.Model):
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
line.invoice_status = 'to invoice'
elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
+ line.product_uom_qty >= 0.0 and\
float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
line.invoice_status = 'upselling'
elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py
index 9572a9c63d..2d7964e34c 100644
--- a/addons/sale/tests/test_sale_order.py
+++ b/addons/sale/tests/test_sale_order.py
@@ -172,6 +172,20 @@ class TestSaleOrder(TestSaleCommon):
self.assertEqual(mail_message.author_id, mail_message.partner_ids, 'Sale: author should be in composer recipients thanks to "partner_to" field set on template')
self.assertEqual(mail_message.partner_ids, mail_message.sudo().mail_ids.recipient_ids, 'Sale: author should receive mail due to presence in composer recipients')
+ def test_invoice_state_when_ordered_quantity_is_negative(self):
+ """When you invoice a SO line with a product that is invoiced on ordered quantities and has negative ordered quantity,
+ this test ensures that the invoicing status of the SO line is 'invoiced' (and not 'upselling')."""
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'product_id': self.company_data['product_order_no'].id,
+ 'product_uom_qty': -1,
+ })]
+ })
+ sale_order.action_confirm()
+ sale_order._create_invoices(final=True)
+ self.assertTrue(sale_order.invoice_status == 'invoiced', 'Sale: The invoicing status of the SO should be "invoiced"')
+
def test_sale_sequence(self):
self.env['ir.sequence'].search([
('code', '=', 'sale.order'),
diff --git a/addons/stock_dropshipping/models/res_company.py b/addons/stock_dropshipping/models/res_company.py
index 05299f022b..010bba4da4 100644
--- a/addons/stock_dropshipping/models/res_company.py
+++ b/addons/stock_dropshipping/models/res_company.py
@@ -90,6 +90,8 @@ class ResCompany(models.Model):
('default_location_src_id.usage', '=', 'supplier'),
('default_location_dest_id.usage', '=', 'customer'),
], limit=1, order='sequence')
+ if not dropship_picking_type:
+ continue
dropship_vals.append({
'name': '%s → %s' % (supplier_location.name, customer_location.name),
'action': 'buy',
diff --git a/addons/web/static/src/js/components/datepicker.js b/addons/web/static/src/js/components/datepicker.js
index bb9b4dcdef..386b4bd526 100644
--- a/addons/web/static/src/js/components/datepicker.js
+++ b/addons/web/static/src/js/components/datepicker.js
@@ -1,6 +1,7 @@
flectra.define('web.DatePickerOwl', function (require) {
"use strict";
+ const config = require('web.config');
const field_utils = require('web.field_utils');
const time = require('web.time');
const { useAutofocus } = require('web.custom_hooks');
@@ -167,7 +168,7 @@ flectra.define('web.DatePickerOwl', function (require) {
* @private
*/
_onWindowScroll(ev) {
- if (ev.target !== this.inputRef.el) {
+ if (!config.device.isIOS && ev.target !== this.inputRef.el) {
this._datetimepicker('hide');
}
}
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 8f26a8eb7e..3b54635718 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -615,16 +615,9 @@ function parseFloat(value) {
* @throws {Error} if no float is found or if parameter does not respect monetary condition
*/
function parseMonetary(value, field, options) {
- var values = value.split(' ');
- if (values.length === 1) {
- values = value.split(NBSP);
- }
- if (values.length === 1) {
+ if (!value.includes(NBSP) && !value.includes(' ')) {
return parseFloat(value);
}
- else if (values.length !== 2) {
- throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
- }
options = options || {};
var currency = options.currency;
if (!currency) {
@@ -635,7 +628,18 @@ function parseMonetary(value, field, options) {
}
currency = session.get_currency(currency_id);
}
- return parseFloat(values[0] === currency.symbol ? values[1] : values[0]);
+ if (!value.includes(currency.symbol)) {
+ throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
+ }
+ if (currency.position === 'before') {
+ return parseFloat(value
+ .replace(`${ currency.symbol }${ NBSP }`, '')
+ .replace(`${ currency.symbol } `, ''));
+ } else {
+ return parseFloat(value
+ .replace(`${ NBSP }${ currency.symbol }`, '')
+ .replace(` ${ currency.symbol }`, ''));
+ }
}
/**
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index d2049a9730..6e5aa7f2a2 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -261,8 +261,9 @@ QUnit.test('parse integer', function(assert) {
});
QUnit.test('parse monetary', function(assert) {
- assert.expect(11);
+ assert.expect(13);
var originalCurrencies = session.currencies;
+ const originalParameters = _.clone(core._t.database.parameters);
session.currencies = {
1: {
digits: [69, 2],
@@ -288,7 +289,18 @@ QUnit.test('parse monetary', function(assert) {
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 1})}, /is not a correct/);
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 34", {}, {currency_id: 3})}, /is not a correct/);
+ // In some languages, the non-breaking space character is used as thousands separator.
+ const nbsp = '\u00a0';
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: nbsp,
+ });
+ assert.strictEqual(fieldUtils.parse.monetary(`1${nbsp}000.00${nbsp}€`, {}, {currency_id: 1}), 1000);
+ assert.strictEqual(fieldUtils.parse.monetary(`$${nbsp}1${nbsp}000.00`, {}, {currency_id: 3}), 1000);
+
session.currencies = originalCurrencies;
+ core._t.database.parameters = originalParameters;
});
QUnit.test('parse percentage', function(assert) {
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 85a13b67c6..65cfa42205 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -77,7 +77,16 @@ function _buildElement(tagName, title, options) {
*/
function _buildTitleElement(title) {
const titleEl = document.createElement('we-title');
- titleEl.textContent = title;
+ // As a stable fix, to not touch XML templates and break existing
+ // translations, the ⌙ character is automatically replaced by └ which makes
+ // more sense for the usecase and should work properly in all browsers. The
+ // ⌙ character is actually rendered mirrored on Windows 11 Chrome (and
+ // others) as the font used for those unicode characters is left to the
+ // browser. We could force a font of our own but it's probably not worth it.
+ // TODO a better solution with a SVG or CSS solution has to be done in
+ // master. That would unify the look of the symbol across all browsers and
+ // also prevent special characters to be placed in translations.
+ titleEl.textContent = title.replace(/⌙/g, '└');
return titleEl;
}
/**
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 58e8a1ff0c..a352385e94 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -1286,10 +1286,14 @@ header {
}
// Language selector
-.js_language_selector {
- .dropdown-menu {
- min-width: 0;
+#wrapwrap:not(.o_rtl) {
+ .js_language_selector {
+ .dropdown-menu {
+ min-width: 0;
+ }
}
+}
+.js_language_selector {
a.list-inline-item {
padding: 3px 0;