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 , do nothing. This is the normal
- // case. The whole code that follows is only there to ensure
- // compatibility with videos added before bug fixes or new Flectra
- // versions where the element is properly saved.
- return def;
+ const proms = [this._super.apply(this, arguments)];
+ let iframeEl = this.$target[0].querySelector(':scope > iframe');
+
+ // The following code is only there to ensure compatibility with
+ // videos added before bug fixes or new Flectra versions where the
+ // element is properly saved.
+ if (!iframeEl) {
+ iframeEl = this._generateIframe();
}
+ if (!iframeEl) {
+ // Something went wrong: no iframe is present in the DOM and the
+ // widget was unable to create one on the fly.
+ return Promise.all(proms);
+ }
+
+ proms.push(this._setupAutoplay(iframeEl.getAttribute('src')));
+ return Promise.all(proms).then(() => {
+ this._triggerAutoplay(iframeEl);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _generateIframe: function () {
// Bug fix / compatibility: empty the element as all information
// to rebuild the iframe should have been saved on the element
this.$target.empty();
@@ -634,25 +704,23 @@ registry.mediaVideo = publicWidget.Widget.extend({
var m = src.match(/^(?:https?:)?\/\/([^/?#]+)/);
if (!m) {
// Unsupported protocol or wrong URL format, don't inject iframe
- return def;
+ return;
}
var domain = m[1].replace(/^www\./, '');
var supportedDomains = ['youtu.be', 'youtube.com', 'youtube-nocookie.com', 'instagram.com', 'vine.co', 'player.vimeo.com', 'vimeo.com', 'dailymotion.com', 'player.youku.com', 'youku.com'];
if (!_.contains(supportedDomains, domain)) {
// Unsupported domain, don't inject iframe
- return def;
+ return;
}
- this.$target.append($('', {
+ return this.$target.append($('', {
src: src,
frameborder: '0',
allowfullscreen: 'allowfullscreen',
- }));
-
- return def;
+ }))[0];
},
});
-registry.backgroundVideo = publicWidget.Widget.extend({
+registry.backgroundVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, {
selector: '.o_background_video',
xmlDependencies: ['/website/static/src/xml/website.background.video.xml'],
disabledInEditableMode: false,
@@ -665,26 +733,13 @@ registry.backgroundVideo = publicWidget.Widget.extend({
this.videoSrc = this.el.dataset.bgVideoSrc;
this.iframeID = _.uniqueId('o_bg_video_iframe_');
-
- this.isYoutubeVideo = this.videoSrc.indexOf('youtube') >= 0;
- this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch;
- if (this.isYoutubeVideo && this.isMobileEnv) {
- this.videoSrc = this.videoSrc + "&enablejsapi=1";
-
- if (!window.YT) {
- var oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady;
- proms.push(new Promise(resolve => {
- window.onYouTubeIframeAPIReady = () => {
- if (oldOnYoutubeIframeAPIReady) {
- oldOnYoutubeIframeAPIReady();
- }
- return resolve();
- };
- }));
- $('', {
- src: 'https://www.youtube.com/iframe_api',
- }).appendTo('head');
- }
+ proms.push(this._setupAutoplay(this.videoSrc));
+ if (this.isYoutubeVideo && this.isMobileEnv && !this.videoSrc.includes('enablejsapi=1')) {
+ // Compatibility: when choosing an autoplay youtube video via the
+ // media manager, the API was not automatically enabled before but
+ // only enabled here in the case of background videos.
+ // TODO migrate those old cases so this code can be removed?
+ this.videoSrc += '&enablejsapi=1';
}
var throttledUpdate = _.throttle(() => this._adjustIframe(), 50);
@@ -785,16 +840,7 @@ registry.backgroundVideo = publicWidget.Widget.extend({
$oldContainer.remove();
this._adjustIframe();
-
- // 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(this.iframeID, {
- events: {
- onReady: ev => ev.target.playVideo(),
- }
- });
- }
+ this._triggerAutoplay(this.$iframe[0]);
},
});
diff --git a/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js b/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js
index 5156c18060..34a4177c07 100644
--- a/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js
+++ b/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js
@@ -2,39 +2,111 @@ flectra.define('website_hr_recruitment.tour', function(require) {
'use strict';
var tour = require("web_tour.tour");
+ function applyForAJob(jobName, application) {
+ return [{
+ content: "Select Job",
+ trigger: `.oe_website_jobs h3 span:contains(${jobName})`,
+ }, {
+ content: "Apply",
+ trigger: ".js_hr_recruitment a:contains('Apply')",
+ }, {
+ content: "Complete name",
+ trigger: "input[name=partner_name]",
+ run: `text ${application.name}`,
+ }, {
+ content: "Complete Email",
+ trigger: "input[name=email_from]",
+ run: `text ${application.email}`,
+ }, {
+ content: "Complete phone number",
+ trigger: "input[name=partner_phone]",
+ run: `text ${application.phone}`,
+ }, {
+ content: "Complete Subject",
+ trigger: "textarea[name=description]",
+ run: `text ${application.subject}`,
+ }, { // TODO: Upload a file ?
+ content: "Send the form",
+ trigger: ".s_website_form_send",
+ }, {
+ content: "Check the form is submitted without errors",
+ trigger: ".oe_structure:has(h1:contains('Congratulations'))",
+ }];
+ }
tour.register('website_hr_recruitment_tour', {
test: true,
url: '/jobs',
+ }, [
+ ...applyForAJob('Guru', {
+ name: 'John Smith',
+ email: 'john@smith.com',
+ phone: '118.218',
+ subject: '### [GURU] HR RECRUITMENT TEST DATA ###',
+ }),
+ {
+ content: "Go back to the jobs page",
+ trigger: "body",
+ run: () => {
+ window.location.href = '/jobs';
+ },
+ },
+ ...applyForAJob('Internship', {
+ name: 'Jack Doe',
+ email: 'jack@doe.com',
+ phone: '118.712',
+ subject: '### HR [INTERN] RECRUITMENT TEST DATA ###',
+ }),
+ ]);
+
+ tour.register('website_hr_recruitment_tour_edit_form', {
+ test: true,
+ url: '/jobs',
}, [{
- content: "Select Job",
- trigger: ".oe_website_jobs h3 span:contains('A Test Job')"
+ content: 'Go to the Guru job page',
+ trigger: 'a[href*="guru"]',
}, {
- content: "Apply",
- trigger: ".js_hr_recruitment a:contains('Apply')"
+ content: 'Go to the Guru job form',
+ trigger: 'a[href*="apply"]',
}, {
- content: "Complete name",
- trigger: "input[name=partner_name]",
- run: "text John Smith"
+ content: 'Check if the Guru form is present',
+ trigger: 'form'
}, {
- content: "Complete Email",
- trigger: "input[name=email_from]",
- run: "text john@smith.com"
+ content: 'Enter in edit mode',
+ trigger: 'a[data-action="edit"]',
}, {
- content: "Complete phone number",
- trigger: "input[name=partner_phone]",
- run: "text 118.218"
+ content: 'Edit the form',
+ trigger: 'input[type="file"]',
+ extra_trigger: '#oe_snippets.o_loaded',
}, {
- content: "Complete Subject",
- trigger: "textarea[name=description]",
- run: "text ### HR RECRUITMENT TEST DATA ###"
- }, { // TODO: Upload a file ?
- content: "Send the form",
- trigger: ".s_website_form_send"
+ content: 'Add a new field',
+ trigger: 'we-button[data-add-field]',
}, {
- content: "Check the form is submited without errors",
- trigger: ".oe_structure:has(h1:contains('Congratulations'))"
- }]);
+ content: 'Save',
+ trigger: 'button[data-action="save"]',
+ }, {
+ content: 'Go back to /jobs page after save',
+ trigger: 'a[data-action="edit"]',
+ run: () => {
+ window.location.href = '/jobs';
+ }
+ }, {
+ content: 'Go to the Internship job page',
+ trigger: 'a[href*="internship"]',
+ }, {
+ content: 'Go to the Internship job form',
+ trigger: 'a[href*="apply"]',
+ }, {
+ content: 'Check that a job_id has been loaded',
+ trigger: 'form',
+ run: () => {
+ const selector = 'input[name="job_id"]:not([value=""])';
+ if (!document.querySelector(selector)) {
+ console.error('The job_id field has a wrong value');
+ }
+ }
+ },
+]);
return {};
});
diff --git a/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py b/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py
index 5e154dd087..02022d9643 100644
--- a/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py
+++ b/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py
@@ -7,16 +7,28 @@ import flectra.tests
@flectra.tests.tagged('post_install', '-at_install')
class TestWebsiteHrRecruitmentForm(flectra.tests.HttpCase):
def test_tour(self):
- job = self.env['hr.job'].create({
- 'name': 'A Test Job',
+ job_guru = self.env['hr.job'].create({
+ 'name': 'Guru',
'is_published': True,
})
-
- self.start_tour("/", 'website_hr_recruitment_tour')
+ job_intern = self.env['hr.job'].create({
+ 'name': 'Internship',
+ 'is_published': True,
+ })
+ self.start_tour('/', 'website_hr_recruitment_tour_edit_form', login='admin')
+ self.start_tour('/', 'website_hr_recruitment_tour')
# check result
- record = self.env['hr.applicant'].search([('description', '=', '### HR RECRUITMENT TEST DATA ###')])
- self.assertEqual(len(record), 1)
- self.assertEqual(record.partner_name, "John Smith")
- self.assertEqual(record.email_from, "john@smith.com")
- self.assertEqual(record.partner_phone, '118.218')
+ guru_applicant = self.env['hr.applicant'].search([('description', '=', '### [GURU] HR RECRUITMENT TEST DATA ###'),
+ ('job_id', '=', job_guru.id),])
+ self.assertEqual(len(guru_applicant), 1)
+ self.assertEqual(guru_applicant.partner_name, 'John Smith')
+ self.assertEqual(guru_applicant.email_from, 'john@smith.com')
+ self.assertEqual(guru_applicant.partner_phone, '118.218')
+
+ internship_applicant = self.env['hr.applicant'].search([('description', '=', '### HR [INTERN] RECRUITMENT TEST DATA ###'),
+ ('job_id', '=', job_intern.id),])
+ self.assertEqual(len(internship_applicant), 1)
+ self.assertEqual(internship_applicant.partner_name, 'Jack Doe')
+ self.assertEqual(internship_applicant.email_from, 'jack@doe.com')
+ self.assertEqual(internship_applicant.partner_phone, '118.712')
|