[IMP] l10n_vn: add Vietnam Bank QR code payment for invoice
Impacted Version: - saas-16.4 and above This commit improve below features: - Add Vietnam Bank QR code payment for invoice based on below github repository https://github.com/anhjean/vietqr task-3344654 closes odoo/odoo#127689 Related: odoo/documentation#5043 Signed-off-by: Nicolas Viseur (vin) <vin@odoo.com>
This commit is contained in:
@@ -29,6 +29,7 @@ CURRENCY_MAPPING = {
|
||||
'RUB': '643',
|
||||
'SAR': '682',
|
||||
'SGD': '702',
|
||||
'VND': '704',
|
||||
'ZAR': '710',
|
||||
'SEK': '752',
|
||||
'CHF': '756',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
],
|
||||
'data': [
|
||||
'data/account_chart_template_data.xml',
|
||||
'views/res_bank_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo_company.xml',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_bank_form_inherit_account" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.form.inherit</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="base.view_partner_bank_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="include_reference" position="after">
|
||||
<p attrs="{'invisible': [('country_code', '!=', 'HK')]}">
|
||||
<a href='https://www.odoo.com/documentation/saas-16.4/applications/finance/fiscal_localizations/hong_kong.html' target='_blank'>Documentation</a>
|
||||
</p>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_bank_form_inherit_account" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.form.inherit</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="base.view_partner_bank_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="include_reference" position="after">
|
||||
<p attrs="{'invisible': [('country_code', '!=', 'SG')]}">
|
||||
<a href='https://www.odoo.com/documentation/saas-16.4/applications/finance/fiscal_localizations/singapore.html' target='_blank'>Documentation</a>
|
||||
</p>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -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',
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import res_bank
|
||||
from . import template_vn
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_l10n_vn_emv_qr
|
||||
@@ -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')
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_bank_form_inherit_account" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.form.inherit</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="base.view_partner_bank_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="include_reference" position="after">
|
||||
<p attrs="{'invisible': [('country_code', '!=', 'VN')]}">
|
||||
<a href='https://www.odoo.com/documentation/saas-16.4/applications/finance/fiscal_localizations/vietnam.html' target='_blank'>Documentation</a>
|
||||
</p>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user