diff --git a/addons/account_qr_code_emv/const.py b/addons/account_qr_code_emv/const.py index 07d7534e078..648a2ab2d71 100644 --- a/addons/account_qr_code_emv/const.py +++ b/addons/account_qr_code_emv/const.py @@ -29,6 +29,7 @@ CURRENCY_MAPPING = { 'RUB': '643', 'SAR': '682', 'SGD': '702', + 'VND': '704', 'ZAR': '710', 'SEK': '752', 'CHF': '756', diff --git a/addons/account_qr_code_emv/models/res_bank.py b/addons/account_qr_code_emv/models/res_bank.py index bd7f8fec056..c243b0d4832 100644 --- a/addons/account_qr_code_emv/models/res_bank.py +++ b/addons/account_qr_code_emv/models/res_bank.py @@ -40,34 +40,35 @@ class ResPartnerBank(models.Model): return crc & 0xFFFF def _get_merchant_account_info(self): - return None + return None, None def _get_additional_data_field(self, comment): return None + def _get_qr_code_vals_list(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication): + tag, merchant_account_info = self._get_merchant_account_info() + currency_code = CURRENCY_MAPPING[currency.name] + merchant_name = self.partner_id.name and self.partner_id.name[:25] or 'NA' + merchant_city = self.partner_id.city and self.partner_id.city[:15] or '' + comment = structured_communication or free_communication or '' + comment = re.sub(r'/[^ A-Za-z0-9_@.\/#&+-]+/g', '', comment) + additional_data_field = self._get_additional_data_field(comment) if self.include_reference else None + return [ + (0, '01'), # Payload Format Indicator + (1, '12'), # Dynamic QR Codes + (tag, merchant_account_info), # Merchant Account Information + (52, '0000'), # Merchant Category Code + (53, currency_code), # Transaction Currency + (54, amount), # Transaction Amount + (58, self.country_code), # Country Code + (59, merchant_name), # Merchant Name + (60, merchant_city), # Merchant City + (62, additional_data_field), # Additional Data Field + ] + def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication): if qr_method == 'emv_qr': - merchant_account_info = self._get_merchant_account_info() - currency_code = CURRENCY_MAPPING[currency.name] - merchant_name = self.partner_id.name and self.partner_id.name[:25] or 'NA' - merchant_city = self.partner_id.city and self.partner_id.city[:15] or '' - comment = structured_communication or free_communication or '' - comment = re.sub(r'/[^ A-Za-z0-9_@.\/#&+-]+/g', '', comment) - additional_data_field = self._get_additional_data_field(comment) if self.include_reference else None - - qr_code_vals = [ - (0, '01'), # Payload Format Indicator - (1, '12'), # Dynamic QR Codes - (26, merchant_account_info), # Merchant Account Information - (52, '0000'), # Merchant Category Code - (53, currency_code), # Transaction Currency - (54, amount), # Transaction Amount - (58, self.country_code), # Country Code - (59, merchant_name), # Merchant Name - (60, merchant_city), # Merchant City - (62, additional_data_field), # Additional Data Field - ] - + qr_code_vals = self._get_qr_code_vals_list(qr_method, amount, currency, debtor_partner, free_communication, structured_communication) qr_code_str = ''.join([self._serialize(*val) for val in qr_code_vals]) qr_code_str += '6304' # CRC16 crc = self._get_crc16(bytes(qr_code_str, 'utf-8')) @@ -93,8 +94,10 @@ class ResPartnerBank(models.Model): return _("Missing Merchant Account Information.") if not self.partner_id.city: return _("Missing Merchant City.") - if not self.proxy_type or not self.proxy_value: - return _("Missing Proxy Type or Proxy Value.") + if not self.proxy_type: + return _("Missing Proxy Type.") + if not self.proxy_value: + return _("Missing Proxy Value.") return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication) @api.model diff --git a/addons/l10n_hk/__manifest__.py b/addons/l10n_hk/__manifest__.py index ef7f5fbb038..c4b23a41b6b 100644 --- a/addons/l10n_hk/__manifest__.py +++ b/addons/l10n_hk/__manifest__.py @@ -12,6 +12,7 @@ ], 'data': [ 'data/account_chart_template_data.xml', + 'views/res_bank_views.xml', ], 'demo': [ 'demo/demo_company.xml', diff --git a/addons/l10n_hk/models/res_bank.py b/addons/l10n_hk/models/res_bank.py index 41401f265c4..5288d75618f 100644 --- a/addons/l10n_hk/models/res_bank.py +++ b/addons/l10n_hk/models/res_bank.py @@ -47,7 +47,7 @@ class ResPartnerBank(models.Model): (fps_type, self.proxy_value), # Proxy Type and Proxy Value ] merchant_account_info = ''.join([self._serialize(*val) for val in merchant_account_vals]) - return merchant_account_info + return (26, merchant_account_info) return super()._get_merchant_account_info() def _get_additional_data_field(self, comment): diff --git a/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py b/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py index d1cb59ff32e..6e921bc5495 100644 --- a/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py +++ b/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py @@ -50,13 +50,7 @@ class TestL10nHKEmvQrCode(AccountTestInvoicingCommon): self.emv_qr_invoice.qr_code_method = 'emv_qr' self.emv_qr_invoice._generate_qr_code() - # Using bank account with non Hong Kong Partner should fail - self.company_data['company'].partner_id.country_id = False - with self.assertRaises(UserError, msg="The chosen QR-code type is not eligible for this invoice."): - self.emv_qr_invoice._generate_qr_code() - # Using invoice currency other than HKD or CNY should fail - self.company_data['company'].partner_id.country_id = self.env.ref('base.hk') self.emv_qr_invoice.currency_id = self.env.ref('base.USD') with self.assertRaises(UserError, msg="The chosen QR-code type is not eligible for this invoice."): self.emv_qr_invoice._generate_qr_code() diff --git a/addons/l10n_hk/views/res_bank_views.xml b/addons/l10n_hk/views/res_bank_views.xml new file mode 100644 index 00000000000..12cca06ba39 --- /dev/null +++ b/addons/l10n_hk/views/res_bank_views.xml @@ -0,0 +1,17 @@ + + + + + res.partner.bank.form.inherit + res.partner.bank + + + +

+ Documentation +

+
+
+
+ +
diff --git a/addons/l10n_sg/__manifest__.py b/addons/l10n_sg/__manifest__.py index ce14507f67a..7c3b356a17f 100644 --- a/addons/l10n_sg/__manifest__.py +++ b/addons/l10n_sg/__manifest__.py @@ -24,6 +24,7 @@ This module add, for accounting: 'data/l10n_sg_chart_data.xml', 'data/account_tax_report_data.xml', 'views/account_invoice_view.xml', + 'views/res_bank_views.xml', 'views/res_company_view.xml', 'views/res_partner_view.xml', ], diff --git a/addons/l10n_sg/models/res_bank.py b/addons/l10n_sg/models/res_bank.py index fdbfdf8be04..ba947e118d6 100644 --- a/addons/l10n_sg/models/res_bank.py +++ b/addons/l10n_sg/models/res_bank.py @@ -35,7 +35,7 @@ class ResPartnerBank(models.Model): (3, 0), # Is Amount Editable ] merchant_account_info = ''.join([self._serialize(*val) for val in merchant_account_vals]) - return merchant_account_info + return (26, merchant_account_info) return super()._get_merchant_account_info() def _get_additional_data_field(self, comment): diff --git a/addons/l10n_sg/tests/test_l10n_sg_emv_qr.py b/addons/l10n_sg/tests/test_l10n_sg_emv_qr.py index 3c76b594f9f..a6b8cfa5bcb 100644 --- a/addons/l10n_sg/tests/test_l10n_sg_emv_qr.py +++ b/addons/l10n_sg/tests/test_l10n_sg_emv_qr.py @@ -45,13 +45,7 @@ class TestL10nSGEmvQrCode(AccountTestInvoicingCommon): self.emv_qr_invoice.qr_code_method = 'emv_qr' self.emv_qr_invoice._generate_qr_code() - # Using bank account with non Singapore Partner should fail - self.company_data['company'].partner_id.country_id = False - with self.assertRaises(UserError, msg="The chosen QR-code type is not eligible for this invoice."): - self.emv_qr_invoice._generate_qr_code() - # Using invoice currency other than SGD should fail - self.company_data['company'].partner_id.country_id = self.env.ref('base.sg') self.emv_qr_invoice.currency_id = self.env.ref('base.USD') with self.assertRaises(UserError, msg="The chosen QR-code type is not eligible for this invoice."): self.emv_qr_invoice._generate_qr_code() diff --git a/addons/l10n_sg/views/res_bank_views.xml b/addons/l10n_sg/views/res_bank_views.xml new file mode 100644 index 00000000000..7bd95286c9a --- /dev/null +++ b/addons/l10n_sg/views/res_bank_views.xml @@ -0,0 +1,17 @@ + + + + + res.partner.bank.form.inherit + res.partner.bank + + + +

+ Documentation +

+
+
+
+ +
diff --git a/addons/l10n_vn/__manifest__.py b/addons/l10n_vn/__manifest__.py index 29a369940a2..bab769ac858 100644 --- a/addons/l10n_vn/__manifest__.py +++ b/addons/l10n_vn/__manifest__.py @@ -8,22 +8,28 @@ 'website': 'https://www.odoo.com/documentation/master/applications/finance/fiscal_localizations.html', 'category': 'Accounting/Localizations/Account Charts', 'description': """ -This is the module to manage the accounting chart for Vietnam in Odoo. -========================================================================= +This is the module to manage the accounting chart, bank information for Vietnam in Odoo. +======================================================================================== -This module applies to companies based in Vietnamese Accounting Standard (VAS) -with Chart of account under Circular No. 200/2014/TT-BTC +- This module applies to companies based in Vietnamese Accounting Standard (VAS) + with Chart of account under Circular No. 200/2014/TT-BTC +- Add Vietnamese bank information (like name, bic ..) as announced and yearly updated by State Bank + of Viet Nam (https://sbv.gov.vn/webcenter/portal/en/home/sbv/paytreasury/bankidno). +- Add VietQR feature for invoice **Credits:** - General Solutions. - Trobz + - Jean Nguyen - The Bean Family (https://github.com/anhjean/vietqr) for VietQR. + """, 'depends': [ - 'account', + 'account_qr_code_emv', 'base_iban', ], 'data': [ 'data/account_tax_report_data.xml', + 'views/res_bank_views.xml', ], 'demo': [ 'demo/demo_company.xml', diff --git a/addons/l10n_vn/models/__init__.py b/addons/l10n_vn/models/__init__.py index 10831bd6576..7f6dcd5f28d 100644 --- a/addons/l10n_vn/models/__init__.py +++ b/addons/l10n_vn/models/__init__.py @@ -1,2 +1,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import res_bank from . import template_vn diff --git a/addons/l10n_vn/models/res_bank.py b/addons/l10n_vn/models/res_bank.py new file mode 100644 index 00000000000..6e502795eae --- /dev/null +++ b/addons/l10n_vn/models/res_bank.py @@ -0,0 +1,68 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResPartnerBank(models.Model): + _inherit = 'res.partner.bank' + + proxy_type = fields.Selection(selection_add=[('merchant_id', 'Merchant ID'), + ('payment_service', 'Payment Service'), + ('atm_card', 'ATM Card Number'), + ('bank_acc', 'Bank Account')], + ondelete={'merchant_id': 'set default', 'payment_service': 'set default', 'atm_card': 'set default', 'bank_acc': 'set default'}) + + @api.constrains('proxy_type') + def _check_vn_proxy(self): + for bank in self.filtered(lambda b: b.country_code == 'VN'): + if bank.proxy_type not in ['merchant_id', 'payment_service', 'atm_card', 'bank_acc', 'none', False]: + raise ValidationError(_("The QR Code Type must be either Merchant ID, ATM Card Number or Bank Account to generate a Vietnam Bank QR code for account number %s.", bank.acc_number)) + + @api.depends('country_code') + def _compute_display_qr_setting(self): + bank_vn = self.filtered(lambda b: b.country_code == 'VN') + bank_vn.display_qr_setting = self.env.company.qr_code + super(ResPartnerBank, self - bank_vn)._compute_display_qr_setting() + + def _get_merchant_account_info(self): + if self.country_code == 'VN': + proxy_type_mapping = { + 'merchant_id': 'QRPUSH', + 'payment_service': 'QRPUSH', + 'atm_card': 'QRIBFTTC', + 'bank_acc': 'QRIBFTTA', + } + payment_network = [ + (0, self.bank_bic), + (1, self.proxy_value), + ] + vals = [ + (0, 'A000000727'), + (1, ''.join([self._serialize(*val) for val in payment_network])), + (2, proxy_type_mapping[self.proxy_type]), + ] + return (38, ''.join([self._serialize(*val) for val in vals])) + return super()._get_merchant_account_info() + + def _get_additional_data_field(self, comment): + if self.country_code == 'VN': + return self._serialize(1, comment) + return super()._get_additional_data_field(comment) + + def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency): + if qr_method == 'emv_qr' and self.country_code == 'VN': + if currency.name not in ['VND']: + return _("Can't generate a Vietnamese QR banking code with a currency other than VND.") + if not self.bank_bic: + return _("Missing Bank Identifier Code.\n" + "Please configure the Bank Identifier Code inside the bank settings.") + return None + + return super()._get_error_messages_for_qr(qr_method, debtor_partner, currency) + + def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication): + if qr_method == 'emv_qr' and self.country_code == 'VN' and self.proxy_type not in ['merchant_id', 'payment_service', 'atm_card', 'bank_acc']: + return _("The proxy type %s is not supported for Vietnamese partners. It must be either Merchant ID, ATM Card Number or Bank Account", self.proxy_type) + + return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication) diff --git a/addons/l10n_vn/tests/__init__.py b/addons/l10n_vn/tests/__init__.py new file mode 100644 index 00000000000..aa36b4a727f --- /dev/null +++ b/addons/l10n_vn/tests/__init__.py @@ -0,0 +1 @@ +from . import test_l10n_vn_emv_qr diff --git a/addons/l10n_vn/tests/test_l10n_vn_emv_qr.py b/addons/l10n_vn/tests/test_l10n_vn_emv_qr.py new file mode 100644 index 00000000000..8bb7c7d47b9 --- /dev/null +++ b/addons/l10n_vn/tests/test_l10n_vn_emv_qr.py @@ -0,0 +1,84 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.exceptions import UserError +from odoo.fields import Command +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import tagged + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestL10nVNEmvQrCode(AccountTestInvoicingCommon): + """ Test the generation of the EMV QR Code on invoices """ + + @classmethod + def setUpClass(cls, chart_template_ref='vn'): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.company_data['company'].qr_code = True + cls.company_data['company'].partner_id.update({ + 'country_id': cls.env.ref('base.vn').id, + 'city': 'Vietnam', + }) + + cls.bank_vn = cls.env['res.bank'].create({ + 'name': 'VN Bank', + 'bic': '970422', + }) + + cls.acc_emv_vn = cls.env['res.partner.bank'].create({ + 'acc_number': '123456789012345678', + 'bank_id': cls.bank_vn.id, + 'partner_id': cls.company_data['company'].partner_id.id, + 'proxy_type': 'bank_acc', + 'proxy_value': '660704060000129', + 'include_reference': True, + }) + + cls.acc_emv_vn_without_paynow_info = cls.env['res.partner.bank'].create({ + 'acc_number': '1234567890', + 'partner_id': cls.company_data['company'].partner_id.id, + }) + + cls.emv_qr_invoice = cls.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': cls.partner_a.id, + 'currency_id': cls.env.ref('base.VND').id, + 'partner_bank_id': cls.acc_emv_vn.id, + 'company_id': cls.company_data['company'].id, + 'invoice_line_ids': [Command.create({'quantity': 1, 'price_unit': 100})], + }) + + def test_emv_qr_code_generation(self): + self.emv_qr_invoice.qr_code_method = 'emv_qr' + self.emv_qr_invoice._generate_qr_code() + + # Using invoice currency other than VND should fail + self.emv_qr_invoice.currency_id = self.env.ref('base.USD') + with self.assertRaises(UserError, msg="The chosen QR-code type is not eligible for this invoice."): + self.emv_qr_invoice._generate_qr_code() + + # Without company partner city should fail + self.emv_qr_invoice.currency_id = self.env.ref('base.VND') + self.company_data['company'].partner_id.city = False + with self.assertRaises(UserError, msg="Missing Merchant City."): + self.emv_qr_invoice._generate_qr_code() + + # Without paynow infomation should fail + self.company_data['company'].partner_id.city = 'Vietnam' + self.emv_qr_invoice.partner_bank_id = self.acc_emv_vn_without_paynow_info + with self.assertRaises(UserError, msg="The account receiving the payment must have a Proxy type and a Proxy value set."): + self.emv_qr_invoice._generate_qr_code() + + def test_emv_qr_vals(self): + self.emv_qr_invoice.qr_code_method = 'emv_qr' + unstruct_ref = 'INV/TEST/0001' + emv_qr_vals = self.emv_qr_invoice.partner_bank_id._get_qr_vals( + qr_method=self.emv_qr_invoice.qr_code_method, + amount=self.emv_qr_invoice.amount_residual, + currency=self.emv_qr_invoice.currency_id, + debtor_partner=self.emv_qr_invoice.partner_id, + free_communication=unstruct_ref, + structured_communication=self.emv_qr_invoice.payment_reference, + ) + + # Check the whole qr code string + self.assertEqual(emv_qr_vals, '00020101021238590010A0000007270129000697042201156607040600001290208QRIBFTTA5204000053037045405100.05802VN5914company_1_data6007Vietnam62170113INV/TEST/0001630425A9') diff --git a/addons/l10n_vn/views/res_bank_views.xml b/addons/l10n_vn/views/res_bank_views.xml new file mode 100644 index 00000000000..a30c03381ad --- /dev/null +++ b/addons/l10n_vn/views/res_bank_views.xml @@ -0,0 +1,17 @@ + + + + + res.partner.bank.form.inherit + res.partner.bank + + + +

+ Documentation +

+
+
+
+ +