From b48ac72eb8ad1c697ef4304cf77dd1094688982b Mon Sep 17 00:00:00 2001 From: Parthiv Patel Date: Fri, 22 Apr 2022 08:35:39 +0000 Subject: [PATCH] [PATCH] Upstream patch - 22042022 --- addons/crm/models/crm_lead.py | 2 +- addons/crm/tests/test_crm_lead.py | 11 + addons/l10n_fi/__manifest__.py | 1 + addons/l10n_fi/tests/test_references.py | 2 +- .../l10n_it_edi/data/invoice_it_template.xml | 26 +- .../l10n_it_edi/models/account_edi_format.py | 3 - .../l10n_it_edi/tests/test_ir_mail_server.py | 260 -------------- addons/l10n_it_edi_sdicoop/tests/__init__.py | 4 + .../expected_xmls/IT00470550013_basis.xml | 73 ++++ .../l10n_it_edi_sdicoop/tests/test_edi_xml.py | 318 ++++++++++++++++++ .../src/components/chat_window/chat_window.js | 1 + .../components/chat_window/chat_window.xml | 1 + .../chat_window_header/chat_window_header.js | 10 + .../chat_window_manager_tests.js | 4 +- addons/payment/models/__init__.py | 1 + addons/payment/models/account_journal.py | 14 + .../js/Screens/ProductScreen/ProductScreen.js | 16 +- addons/purchase/tests/test_access_rights.py | 11 +- addons/sale/views/res_partner_views.xml | 2 +- .../views/hr_timesheet_templates.xml | 1 + .../views/survey_templates_statistics.xml | 2 +- .../web/static/src/js/fields/basic_fields.js | 4 +- .../static/src/js/views/basic/basic_model.js | 13 +- .../static/tests/fields/basic_fields_tests.js | 5 + .../tests/fields/relational_fields_tests.js | 50 +++ addons/web/static/tests/views/form_tests.js | 38 +++ .../static/src/js/wysiwyg/widgets/media.js | 8 +- .../src/js/content/snippets.animation.js | 136 +++++--- .../tests/tours/website_hr_recruitment.js | 116 +++++-- .../tests/test_website_hr_recruitment.py | 30 +- 30 files changed, 797 insertions(+), 366 deletions(-) create mode 100644 addons/l10n_it_edi_sdicoop/tests/__init__.py create mode 100644 addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml create mode 100644 addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py create mode 100644 addons/payment/models/account_journal.py diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 9d6114b96b..5760e2b491 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -589,7 +589,7 @@ class Lead(models.Model): # stage change with new stage: update probability and date_closed if vals.get('probability', 0) >= 100 or not vals.get('active', True): vals['date_closed'] = fields.Datetime.now() - elif 'probability' in vals: + elif tools.float_compare(vals.get('probability', 0), 0, precision_digits=2) > 0: vals['date_closed'] = False if any(field in ['active', 'stage_id'] for field in vals): diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py index 1e8050a20f..d9e91cc2b9 100644 --- a/addons/crm/tests/test_crm_lead.py +++ b/addons/crm/tests/test_crm_lead.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from datetime import datetime +from freezegun import freeze_time + from flectra.addons.crm.models.crm_lead import PARTNER_FIELDS_TO_SYNC, PARTNER_ADDRESS_FIELDS_TO_SYNC from flectra.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL from flectra.addons.phone_validation.tools.phone_validation import phone_format @@ -370,6 +373,14 @@ class TestCRMLead(TestCrmCommon): self.assertEqual(lead.probability, 100.0) self.assertEqual(lead.stage_id, self.stage_gen_won) # generic won stage has lower sequence than team won stage + @users('user_sales_leads') + @freeze_time("2012-01-14") + def test_crm_lead_lost_date_closed(self): + self.assertFalse(self.lead_1.date_closed, "Initially, closed date is not set") + # Mark the lead as lost + self.lead_1.action_set_lost() + self.assertEqual(self.lead_1.date_closed, datetime.now(), "Closed date is updated after marking lead as lost") + @users('user_sales_leads') def test_crm_lead_update_contact(self): # ensure initial data, especially for corner cases diff --git a/addons/l10n_fi/__manifest__.py b/addons/l10n_fi/__manifest__.py index fdb9567ea9..8fac182078 100644 --- a/addons/l10n_fi/__manifest__.py +++ b/addons/l10n_fi/__manifest__.py @@ -17,6 +17,7 @@ After installing this module, you'll have access to : * Finnish chart of account * Fiscal positions * Invoice Payment Reference Types (Finnish Standard Reference & Finnish Creditor Reference (RF)) + * Finnish Reference format for Sale Orders Set the payment reference type from the Sales Journal. """, diff --git a/addons/l10n_fi/tests/test_references.py b/addons/l10n_fi/tests/test_references.py index 8efe1971ba..7038bb1a32 100644 --- a/addons/l10n_fi/tests/test_references.py +++ b/addons/l10n_fi/tests/test_references.py @@ -4,7 +4,7 @@ from flectra.addons.account.tests.common import AccountTestInvoicingCommon from flectra.exceptions import UserError -@tagged('standard', 'at_install') +@tagged('post_install', '-at_install') class PaymentReferenceTest(AccountTestInvoicingCommon): """ All references validated with the reference calculator by Nordea Bank diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml index 784d7ca843..82ea7880da 100644 --- a/addons/l10n_it_edi/data/invoice_it_template.xml +++ b/addons/l10n_it_edi/data/invoice_it_template.xml @@ -20,7 +20,7 @@ - + @@ -176,19 +176,21 @@ - + + TP02TP01 - - - MP05 - - - - - - - + + + MP05 + + + + + + + + diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py index c9e6d8e2e4..6bc9ba0ac8 100644 --- a/addons/l10n_it_edi/models/account_edi_format.py +++ b/addons/l10n_it_edi/models/account_edi_format.py @@ -120,9 +120,6 @@ class AccountEdiFormat(models.Model): if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0: errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name)) - if not invoice.partner_bank_id: - errors.append(_("The seller must have a bank account.")) - return errors # ------------------------------------------------------------------------- diff --git a/addons/l10n_it_edi/tests/test_ir_mail_server.py b/addons/l10n_it_edi/tests/test_ir_mail_server.py index 77885e5d31..38da986651 100644 --- a/addons/l10n_it_edi/tests/test_ir_mail_server.py +++ b/addons/l10n_it_edi/tests/test_ir_mail_server.py @@ -5,7 +5,6 @@ import datetime import logging from collections import namedtuple from unittest.mock import patch -from lxml import etree from freezegun import freeze_time from flectra import tools @@ -52,7 +51,6 @@ class PecMailServerTests(AccountEdiTestCommon): # Initialize the company's codice fiscale cls.company.l10n_it_codice_fiscale = '01234560157' - cls.company.vat = 'IT01234560157' # Build test data. # invoice_filename1 is used for vendor bill receipts tests @@ -85,97 +83,9 @@ class PecMailServerTests(AccountEdiTestCommon): 'server_type': 'imap', 'l10n_it_is_pec': True}) - cls.price_included_tax = cls.env['account.tax'].create({ - 'name': '22% price included tax', - 'amount': 22.0, - 'amount_type': 'percent', - 'price_include': True, - 'include_base_amount': True, - 'company_id': cls.company.id, - }) - - cls.italian_partner_a = cls.env['res.partner'].create({ - 'name': 'Alessi', - 'vat': 'IT00465840031', - 'l10n_it_codice_fiscale': '00465840031', - 'country_id': cls.env.ref('base.it').id, - 'street': 'Via Privata Alessi 6', - 'zip': '28887', - 'company_id': cls.company.id, - }) - - cls.standard_line = { - 'name': 'standard_line', - 'quantity': 1, - 'price_unit': 800.40, - 'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])] - } - - cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({ - 'move_type': 'out_invoice', - 'invoice_date': datetime.date(2022, 3, 24), - 'partner_id': cls.italian_partner_a.id, - 'invoice_line_ids': [ - (0, 0, { - **cls.standard_line, - 'name': 'something price included', - 'tax_ids': [(6, 0, [cls.price_included_tax.id])] - }), - (0, 0, { - **cls.standard_line, - 'name': 'something else price included', - 'tax_ids': [(6, 0, [cls.price_included_tax.id])] - }), - (0, 0, { - **cls.standard_line, - 'name': 'something not price included', - }), - ], - }) - - cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ - 'move_type': 'out_invoice', - 'invoice_date': datetime.date(2022, 3, 24), - 'partner_id': cls.italian_partner_a.id, - 'invoice_line_ids': [ - (0, 0, { - **cls.standard_line, - 'name': 'no discount', - }), - (0, 0, { - **cls.standard_line, - 'name': 'special discount', - 'discount': 50, - }), - (0, 0, { - **cls.standard_line, - 'name': "an offer you can't refuse", - 'discount': 100, - }), - ], - }) - - cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ - 'move_type': 'out_invoice', - 'invoice_date': datetime.date(2022, 3, 24), - 'partner_id': cls.italian_partner_a.id, - 'invoice_line_ids': [ - (0, 0, { - **cls.standard_line, - 'name': 'nothing shady just a gift for my friend', - 'discount': 100, - }), - ], - }) - # post the invoices - cls.price_included_invoice._post() - cls.partial_discount_invoice._post() - cls.full_discount_invoice._post() - cls.test_invoice_xmls = {k: cls._get_test_file_content(v) for k, v in [ ('normal_1', 'IT01234567890_FPR01.xml'), ('signed', 'IT01234567890_FPR01.xml.p7m'), - ('export_basis', 'IT00470550013_basis.xml'), ]} @classmethod @@ -254,173 +164,3 @@ class PecMailServerTests(AccountEdiTestCommon): def test_decorrenza_termini(self): """ Test a receipt adapted from https://www.fatturapa.gov.it/export/documenti/messaggi/v1.0/IT01234567890_11111_DT_001.xml """ self._test_receipt('DT', 'delivered', 'delivered_expired') - - @freeze_time('2020-03-24') - def test_price_included_taxes(self): - """ When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not - equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also - taxable base = sum(subtotals) + rounding amount) - """ - - # In this case, the first two lines use a price_include tax the - # subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564.., - # where 22.0 is the tax rate. - # - # Since the subtotals are rounded we actually have 656.07 - lines = self.price_included_invoice.line_ids - price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax) - self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07]) - # So the taxable a base the edi expects (for this tax) is actually 1312.14 - price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax) - self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14) - - # The tax amount of the price included tax should be: - # per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33 - # tax amount: 144.33 * 2 = 288.66 - self.assertEqual(price_included_tax_line.price_total, 288.66) - - expected_etree = self.with_applied_xpath( - etree.fromstring(self.test_invoice_xmls['export_basis']), - ''' - - - - 1 - something price included - 1.00 - 656.070000 - 656.07 - 22.00 - - - 2 - something else price included - 1.00 - 656.070000 - 656.07 - 22.00 - - - 3 - something not price included - 1.00 - 800.400000 - 800.40 - 22.00 - - - 22.00 - -0.04909091 - 1312.09 - 288.66 - I - - - 22.00 - 800.40 - 176.09 - I - - - - - 2577.29 - - ''') - invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml()) - # Remove the attachment and its details - invoice_etree = self.with_applied_xpath(invoice_etree, "") - self.assertXmlTreeEqual(invoice_etree, expected_etree) - - @freeze_time('2020-03-24') - def test_partially_discounted_invoice(self): - # The EDI can account for discounts, but a line with, for example, a 100% discount should still have - # a corresponding tax with a base amount of 0 - - invoice_etree = etree.fromstring(self.partial_discount_invoice._export_as_xml()) - expected_etree = self.with_applied_xpath( - etree.fromstring(self.test_invoice_xmls['export_basis']), - ''' - - - - 1 - no discount - 1.00 - 800.400000 - 800.40 - 22.00 - - - 2 - special discount - 1.00 - 800.400000 - - SC - 50.00 - - 400.20 - 22.00 - - - 3 - an offer you can't refuse - 1.00 - 800.400000 - - SC - 100.00 - - 0.00 - 22.00 - - - 22.00 - 1200.60 - 264.13 - I - - - - - 1464.73 - - ''') - invoice_etree = self.with_applied_xpath(invoice_etree, "") - self.assertXmlTreeEqual(invoice_etree, expected_etree) - - @freeze_time('2020-03-24') - def test_fully_discounted_inovice(self): - invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml()) - expected_etree = self.with_applied_xpath( - etree.fromstring(self.test_invoice_xmls['export_basis']), - ''' - - - - 1 - nothing shady just a gift for my friend - 1.00 - 800.400000 - - SC - 100.00 - - 0.00 - 22.00 - - - 22.00 - 0.00 - 0.00 - I - - - - - 0.00 - - ''') - invoice_etree = self.with_applied_xpath(invoice_etree, "") - self.assertXmlTreeEqual(invoice_etree, expected_etree) diff --git a/addons/l10n_it_edi_sdicoop/tests/__init__.py b/addons/l10n_it_edi_sdicoop/tests/__init__.py new file mode 100644 index 0000000000..0d6f6d825d --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from . import test_edi_xml diff --git a/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml new file mode 100644 index 0000000000..56c49b8793 --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml @@ -0,0 +1,73 @@ + + + + + IT + 01234560157 + + ___ignore___ + FPR12 + 0000000 + + + + + + + IT + 01234560157 + + 01234560157 + + company_2_data + + RF01 + + + 1234 Test Street + 12345 + Prova + IT + + + + + + IT + 00465840031 + + + Alessi + + + + + Via Privata Alessi 6 + 28887 + Milan + IT + + + + + + + TD01 + EUR + 2022-03-24 + ___ignore___ + + + + + + TP02 + + MP05 + 2022-03-24 + + ___ignore___ + + + + diff --git a/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py new file mode 100644 index 0000000000..3e4675adfe --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +import datetime +import logging +from lxml import etree +from freezegun import freeze_time + +from flectra import tools +from flectra.tests import tagged +from flectra.addons.account_edi.tests.common import AccountEdiTestCommon + +_logger = logging.getLogger(__name__) + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestItEdi(AccountEdiTestCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic', + edi_format_ref='l10n_it_edi.edi_fatturaPA') + + # Use the company_data_2 to test that the e-invoice is imported for the right company + cls.company = cls.company_data_2['company'] + + cls.company.l10n_it_codice_fiscale = '01234560157' + cls.company.vat = 'IT01234560157' + cls.test_bank = cls.env['res.partner.bank'].with_company(cls.company).create({ + 'partner_id': cls.company.partner_id.id, + 'acc_number': 'IT1212341234123412341234123', + 'bank_name': 'BIG BANK', + 'bank_bic': 'BIGGBANQ', + }) + cls.company.l10n_it_tax_system = "RF01" + cls.company.street = "1234 Test Street" + cls.company.zip = "12345" + cls.company.city = "Prova" + cls.company.country_id = cls.env.ref('base.it') + + cls.price_included_tax = cls.env['account.tax'].create({ + 'name': '22% price included tax', + 'amount': 22.0, + 'amount_type': 'percent', + 'price_include': True, + 'include_base_amount': True, + 'company_id': cls.company.id, + }) + + cls.italian_partner_a = cls.env['res.partner'].create({ + 'name': 'Alessi', + 'vat': 'IT00465840031', + 'l10n_it_codice_fiscale': '00465840031', + 'country_id': cls.env.ref('base.it').id, + 'street': 'Via Privata Alessi 6', + 'zip': '28887', + 'city': 'Milan', + 'company_id': cls.company.id, + }) + + cls.standard_line = { + 'name': 'standard_line', + 'quantity': 1, + 'price_unit': 800.40, + 'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])] + } + + cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({ + 'move_type': 'out_invoice', + 'invoice_date': datetime.date(2022, 3, 24), + 'partner_id': cls.italian_partner_a.id, + 'partner_bank_id': cls.test_bank.id, + 'invoice_line_ids': [ + (0, 0, { + **cls.standard_line, + 'name': 'something price included', + 'tax_ids': [(6, 0, [cls.price_included_tax.id])] + }), + (0, 0, { + **cls.standard_line, + 'name': 'something else price included', + 'tax_ids': [(6, 0, [cls.price_included_tax.id])] + }), + (0, 0, { + **cls.standard_line, + 'name': 'something not price included', + }), + ], + }) + + cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ + 'move_type': 'out_invoice', + 'invoice_date': datetime.date(2022, 3, 24), + 'partner_id': cls.italian_partner_a.id, + 'partner_bank_id': cls.test_bank.id, + 'invoice_line_ids': [ + (0, 0, { + **cls.standard_line, + 'name': 'no discount', + }), + (0, 0, { + **cls.standard_line, + 'name': 'special discount', + 'discount': 50, + }), + (0, 0, { + **cls.standard_line, + 'name': "an offer you can't refuse", + 'discount': 100, + }), + ], + }) + + cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ + 'move_type': 'out_invoice', + 'invoice_date': datetime.date(2022, 3, 24), + 'partner_id': cls.italian_partner_a.id, + 'partner_bank_id': cls.test_bank.id, + 'invoice_line_ids': [ + (0, 0, { + **cls.standard_line, + 'name': 'nothing shady just a gift for my friend', + 'discount': 100, + }), + ], + }) + + # We create this because we are unable to post without a proxy user existing + cls.proxy_user = cls.env['account_edi_proxy_client.user'].create({ + 'id_client': 'l10n_it_edi_sdicoop_test', + 'company_id': cls.company.id, + 'edi_format_id': cls.edi_format.id, + 'edi_identification': 'l10n_it_edi_sdicoop_test', + 'private_key': 'l10n_it_edi_sdicoop_test', + }) + + # post the invoices + cls.price_included_invoice._post() + cls.partial_discount_invoice._post() + cls.full_discount_invoice._post() + + cls.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml') + + @classmethod + def _get_test_file_content(cls, filename): + """ Get the content of a test file inside this module """ + path = 'l10n_it_edi_sdicoop/tests/expected_xmls/' + filename + with tools.file_open(path, mode='rb') as test_file: + return test_file.read() + + @freeze_time('2020-03-24') + def test_price_included_taxes(self): + """ When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not + equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also + taxable base = sum(subtotals) + rounding amount) + """ + + # In this case, the first two lines use a price_include tax the + # subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564.., + # where 22.0 is the tax rate. + # + # Since the subtotals are rounded we actually have 656.07 + lines = self.price_included_invoice.line_ids + price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax) + self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07]) + # So the taxable a base the edi expects (for this tax) is actually 1312.14 + price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax) + self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14) + + # The tax amount of the price included tax should be: + # per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33 + # tax amount: 144.33 * 2 = 288.66 + self.assertEqual(price_included_tax_line.price_total, 288.66) + + expected_etree = self.with_applied_xpath( + etree.fromstring(self.edi_basis_xml), + ''' + + + + 1 + something price included + 1.00 + 656.070000 + 656.07 + 22.00 + + + 2 + something else price included + 1.00 + 656.070000 + 656.07 + 22.00 + + + 3 + something not price included + 1.00 + 800.400000 + 800.40 + 22.00 + + + 22.00 + -0.04909091 + 1312.09 + 288.66 + I + + + 22.00 + 800.40 + 176.09 + I + + + + + 2577.29 + + ''') + invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml()) + # Remove the attachment and its details + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree) + + @freeze_time('2020-03-24') + def test_partially_discounted_invoice(self): + # The EDI can account for discounts, but a line with, for example, a 100% discount should still have + # a corresponding tax with a base amount of 0 + + invoice_etree = etree.fromstring(self.partial_discount_invoice._export_as_xml()) + expected_etree = self.with_applied_xpath( + etree.fromstring(self.edi_basis_xml), + ''' + + + + 1 + no discount + 1.00 + 800.400000 + 800.40 + 22.00 + + + 2 + special discount + 1.00 + 800.400000 + + SC + 50.00 + + 400.20 + 22.00 + + + 3 + an offer you can't refuse + 1.00 + 800.400000 + + SC + 100.00 + + 0.00 + 22.00 + + + 22.00 + 1200.60 + 264.13 + I + + + + + 1464.73 + + ''') + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree) + + @freeze_time('2020-03-24') + def test_fully_discounted_inovice(self): + invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml()) + expected_etree = self.with_applied_xpath( + etree.fromstring(self.edi_basis_xml), + ''' + + + + 1 + nothing shady just a gift for my friend + 1.00 + 800.400000 + + SC + 100.00 + + 0.00 + 22.00 + + + 22.00 + 0.00 + 0.00 + I + + + + + 0.00 + + ''') + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree) diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js index 888db097b6..39dc0936c1 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.js +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -67,6 +67,7 @@ class ChatWindow extends Component { // the following are passed as props to children this._onAutocompleteSelect = this._onAutocompleteSelect.bind(this); this._onAutocompleteSource = this._onAutocompleteSource.bind(this); + this._saveThreadScrollTop = this._saveThreadScrollTop.bind(this); this._constructor(...args); } diff --git a/addons/mail/static/src/components/chat_window/chat_window.xml b/addons/mail/static/src/components/chat_window/chat_window.xml index ad4a10962e..a0f0b4b8c3 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.xml +++ b/addons/mail/static/src/components/chat_window/chat_window.xml @@ -17,6 +17,7 @@ chatWindowLocalId="chatWindow.localId" hasCloseAsBackButton="props.hasCloseAsBackButton" isExpandable="props.isExpandable" + saveThreadScrollTop="_saveThreadScrollTop" t-on-o-clicked="_onClickedHeader" t-ref="header" /> diff --git a/addons/mail/static/src/components/chat_window_header/chat_window_header.js b/addons/mail/static/src/components/chat_window_header/chat_window_header.js index 923e3d724c..67b21bedc3 100644 --- a/addons/mail/static/src/components/chat_window_header/chat_window_header.js +++ b/addons/mail/static/src/components/chat_window_header/chat_window_header.js @@ -85,6 +85,9 @@ class ChatWindowHeader extends Component { */ _onClickShiftLeft(ev) { ev.stopPropagation(); + if (this.props.saveThreadScrollTop) { + this.props.saveThreadScrollTop(); + } this.chatWindow.shiftLeft(); } @@ -94,6 +97,9 @@ class ChatWindowHeader extends Component { */ _onClickShiftRight(ev) { ev.stopPropagation(); + if (this.props.saveThreadScrollTop) { + this.props.saveThreadScrollTop(); + } this.chatWindow.shiftRight(); } @@ -109,6 +115,10 @@ Object.assign(ChatWindowHeader, { chatWindowLocalId: String, hasCloseAsBackButton: Boolean, isExpandable: Boolean, + saveThreadScrollTop: { + type: Function, + optional: true, + }, }, template: 'mail.ChatWindowHeader', }); diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js index 46cd5d52a0..022ccc6bae 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -2416,7 +2416,7 @@ QUnit.test('chat window should remain folded when new message is received', asyn ); }); -QUnit.test('chat window scroll position should remain the same after switching left', async function (assert) { +QUnit.skip('chat window scroll position should remain the same after switching left', async function (assert) { assert.expect(2); this.data['mail.channel'].records.push({ @@ -2474,7 +2474,7 @@ QUnit.test('chat window scroll position should remain the same after switching l ); }); -QUnit.test('chat window scroll position should remain the same after switching right', async function (assert) { +QUnit.skip('chat window scroll position should remain the same after switching right', async function (assert) { assert.expect(2); this.data['mail.channel'].records.push({ diff --git a/addons/payment/models/__init__.py b/addons/payment/models/__init__.py index 8491af3496..955d80e9b4 100644 --- a/addons/payment/models/__init__.py +++ b/addons/payment/models/__init__.py @@ -2,6 +2,7 @@ from . import payment_acquirer from . import account_invoice +from . import account_journal from . import res_partner from . import account_payment from . import chart_template diff --git a/addons/payment/models/account_journal.py b/addons/payment/models/account_journal.py new file mode 100644 index 0000000000..b0e863dc9b --- /dev/null +++ b/addons/payment/models/account_journal.py @@ -0,0 +1,14 @@ +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, models, _ +from flectra.exceptions import ValidationError + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + @api.constrains('type') + def _check_journal_type_change(self): + acquirer_incompatible_journals = self.filtered(lambda j: j.type not in ('bank', 'cash')) + if acquirer_incompatible_journals and self.env['payment.acquirer'].search_count([('journal_id', 'in', acquirer_incompatible_journals.ids)]): + raise ValidationError(_("An acquirer is using this journal. Only bank and cash types are allowed.")) diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js index 4606d474e5..753c954f23 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -310,8 +310,20 @@ flectra.define('point_of_sale.ProductScreen', function(require) { this.currentOrder.updatePricelist(newClient); } } - _onClickPay() { - this.showScreen('PaymentScreen'); + async _onClickPay() { + if (this.env.pos.get_order().orderlines.any(line => line.get_product().tracking !== 'none' && !line.has_valid_product_lot() && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots))) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: this.env._t('Some Serial/Lot Numbers are missing'), + body: this.env._t('You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?'), + confirmText: this.env._t('Yes'), + cancelText: this.env._t('No') + }); + if (confirmed) { + this.showScreen('PaymentScreen'); + } + } else { + this.showScreen('PaymentScreen'); + } } switchPane() { if (this.mobile_pane === "left") { diff --git a/addons/purchase/tests/test_access_rights.py b/addons/purchase/tests/test_access_rights.py index dd61b74c03..53707d3dc0 100644 --- a/addons/purchase/tests/test_access_rights.py +++ b/addons/purchase/tests/test_access_rights.py @@ -142,7 +142,16 @@ class TestPurchaseInvoice(AccountTestInvoicingCommon): """Only purchase managers can approve a purchase order when double validation is enabled""" group_purchase_manager = self.env.ref('purchase.group_purchase_manager') - order = self.env.ref("purchase.purchase_order_1") + order = self.env['purchase.order'].create({ + "partner_id": self.vendor.id, + "order_line": [ + (0, 0, { + 'product_id': self.product.id, + 'name': f'{self.product.name} {1:05}', + 'price_unit': 79.80, + 'product_qty': 15.0, + }), + ]}) company = order.sudo().company_id company.po_double_validation = 'two_step' company.po_double_validation_amount = 0 diff --git a/addons/sale/views/res_partner_views.xml b/addons/sale/views/res_partner_views.xml index e01c323a06..162c2598fd 100644 --- a/addons/sale/views/res_partner_views.xml +++ b/addons/sale/views/res_partner_views.xml @@ -51,7 +51,7 @@ + attrs="{'required':[('sale_warn', '!=', False), ('sale_warn','!=','no-message')], 'invisible':[('sale_warn','in',(False,'no-message'))]}"/> diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index 4cea40e0ff..a037ded954 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -132,6 +132,7 @@ data-model="project.task" data-views='[[false, "list"], [false, "form"]]' t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])" + t-att-data-context="json.dumps({'active_test': False})" > - % + % ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 1, + }); + + await testUtils.form.clickEdit(form); + await testUtils.fields.many2one.clickOpenDropdown("turtle_trululu"); + await testUtils.fields.many2one.searchAndClickItem('turtle_trululu', {search: 'first record'}); + + const getElementTextContent = name => [...document.querySelectorAll(`.o_field_many2manytags[name="${name}"] .badge.o_tag_color_0 > span`)] + .map(x=>x.textContent); + assert.deepEqual( + getElementTextContent('product_partner_ids'), + ['first record'], + "should have the correct value in the many2many tag widget"); + assert.deepEqual( + getElementTextContent('partner_ids'), + ['first record', 'second record'], + "should have the correct values in the many2many tag widget"); + form.destroy(); + }); }); }); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 39999349f8..a9b3dad8f0 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2352,6 +2352,44 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('remove default value in subviews', async function (assert) { + assert.expect(2); + + this.data.product.onchanges = {} + this.data.product.onchanges.name = function () {}; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + viewOptions: { + context: {default_state: "ab"} + }, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (route === "/web/dataset/call_kw/partner/onchange") { + assert.deepEqual(args.kwargs.context, { + default_state: 'ab', + }) + } + else if (route === "/web/dataset/call_kw/product/onchange") { + assert.deepEqual(args.kwargs.context, { + default_product_uom_qty: 68, + }) + } + return this._super.apply(this, arguments); + }, + }); + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + form.destroy(); + }); + QUnit.test('reference field in one2many list', async function (assert) { assert.expect(1); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index dde9e4e7c7..932cc1d35d 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -1423,7 +1423,13 @@ var VideoWidget = MediaWidget.extend({ const fullscreen = options.hide_fullscreen ? '&fs=0' : ''; const ytLoop = loop ? loop + `&playlist=${matches.youtube[2]}` : ''; const logo = options.hide_yt_logo ? '&modestbranding=1' : ''; - embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}&rel=0${ytLoop}${controls}${fullscreen}${logo}`; + // The youtube js api is needed for autoplay on mobile. Note: this + // was added as a fix, old customers may have autoplay videos + // without this, which will make their video autoplay on desktop + // but not in mobile (so no behavior change was done in stable, + // this should not be migrated). + const enablejsapi = options.autoplay ? '&enablejsapi=1' : ''; + embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}${enablejsapi}&rel=0${ytLoop}${controls}${fullscreen}${logo}`; type = 'youtube'; } else if (matches.instagram && matches.instagram[2].length) { embedURL = `//www.instagram.com/p/${matches.instagram[2]}/embed/`; diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index ce9dab9afa..c487d065c1 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -5,6 +5,7 @@ flectra.define('website.content.snippets.animation', function (require) { * Provides a way to start JS code for snippets' initialization and animations. */ +const ajax = require('web.ajax'); var Class = require('web.Class'); var config = require('web.config'); var core = require('web.core'); @@ -596,7 +597,55 @@ registry.Parallax = Animation.extend({ }, }); -registry.mediaVideo = publicWidget.Widget.extend({ +const MobileYoutubeAutoplayMixin = { + /** + * Takes care of any necessary setup for autoplaying video. In practice, + * this method will load the youtube iframe API for mobile environments + * because mobile environments don't support the youtube autoplay param + * passed in the url. + * + * @private + * @param {string} src - The source url of the video + */ + _setupAutoplay: function (src) { + let promise = Promise.resolve(); + + this.isYoutubeVideo = src.indexOf('youtube') >= 0; + this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch; + + if (this.isYoutubeVideo && this.isMobileEnv && !window.YT) { + const oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady; + promise = new Promise(resolve => { + window.onYouTubeIframeAPIReady = () => { + if (oldOnYoutubeIframeAPIReady) { + oldOnYoutubeIframeAPIReady(); + } + return resolve(); + }; + }); + ajax.loadJS('https://www.youtube.com/iframe_api'); + } + + return promise; + }, + /** + * @private + * @param {DOMElement} iframeEl - the iframe containing the video player + */ + _triggerAutoplay: function (iframeEl) { + // YouTube does not allow to auto-play video in mobile devices, so we + // have to play the video manually. + if (this.isMobileEnv && this.isYoutubeVideo) { + new window.YT.Player(iframeEl, { + events: { + onReady: ev => ev.target.playVideo(), + } + }); + } + }, +}; + +registry.mediaVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, { selector: '.media_iframe_video', /** @@ -606,15 +655,36 @@ registry.mediaVideo = publicWidget.Widget.extend({ // TODO: this code should be refactored to make more sense and be better // integrated with Flectra (this refactoring should be done in master). - var def = this._super.apply(this, arguments); - if (this.$target.children('iframe').length) { - // There already is an