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
+
+
+
+
+
+