[IMP] l10n_hk: added FPS QR code payment for invoice
- Add FPS QR code payment by following the government document of FPS QR Code Standard - https://www.hkma.gov.hk/media/eng/doc/key-functions/financial-infrastructure/infrastructure/retail-payment-initiatives/Common_QR_Code_Specification.pdf task-3042940 closes odoo/odoo#120050 Related: odoo/documentation#4309 Signed-off-by: Josse Colpaert <jco@odoo.com>
This commit is contained in:
@@ -8,10 +8,11 @@
|
||||
'category': 'Accounting/Localizations/Account Charts',
|
||||
'description': ' This is the base module to manage chart of accounting and localization for Hong Kong ',
|
||||
'depends': [
|
||||
'account',
|
||||
'account_qr_code_emv',
|
||||
],
|
||||
'data': [
|
||||
'data/account_chart_template_data.xml',
|
||||
'views/res_bank_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo_company.xml',
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_hk
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-03-07 09:39+0000\n"
|
||||
"PO-Revision-Date: 2023-03-07 09:39+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model,name:l10n_hk.model_res_partner_bank
|
||||
msgid "Bank Accounts"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_account_setup_bank_manual_config__country_code
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_res_partner_bank__country_code
|
||||
msgid "Country Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields.selection,name:l10n_hk.selection__res_partner_bank__l10n_hk_fps_type__email
|
||||
msgid "Email Address"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields.selection,name:l10n_hk.selection__res_partner_bank__l10n_hk_fps_type__id
|
||||
msgid "FPS ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_account_setup_bank_manual_config__l10n_hk_fps_identifier
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_res_partner_bank__l10n_hk_fps_identifier
|
||||
msgid "FPS ID/Phone Number/Email Address"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_account_setup_bank_manual_config__l10n_hk_fps_type
|
||||
#: model:ir.model.fields,field_description:l10n_hk.field_res_partner_bank__l10n_hk_fps_type
|
||||
msgid "FPS Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_hk/models/res_bank.py:0
|
||||
#, python-format
|
||||
msgid "Invalid Email! Please enter a valid email address."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_hk/models/res_bank.py:0
|
||||
#, python-format
|
||||
msgid "Invalid FPS ID! Please enter a valid FPS ID with length 7 or 9."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_hk/models/res_bank.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invalid Mobile! Please enter a valid mobile number with format "
|
||||
"+852-67891234."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields.selection,name:l10n_hk.selection__res_partner_bank__l10n_hk_fps_type__phone
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model:ir.model.fields,help:l10n_hk.field_account_setup_bank_manual_config__country_code
|
||||
#: model:ir.model.fields,help:l10n_hk.field_res_partner_bank__country_code
|
||||
msgid ""
|
||||
"The ISO country code in two chars. \n"
|
||||
"You can use this field for quick search."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_hk/models/res_bank.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The account receiving the payment must have a FPS type and a FPS identifier "
|
||||
"set."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_hk
|
||||
#: model_terms:ir.ui.view,arch_db:l10n_hk.view_partner_bank_form_inherit_account
|
||||
msgid "e.g. +852-67891234"
|
||||
msgstr ""
|
||||
@@ -1,2 +1,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import res_bank
|
||||
from . import template_hk
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import single_email_re
|
||||
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
country_code = fields.Char(related='partner_id.country_code', string="Country Code")
|
||||
l10n_hk_fps_type = fields.Selection([('id', "FPS ID"), ('mobile', "Mobile Number"), ('email', "Email Address")], string='FPS Type')
|
||||
l10n_hk_fps_identifier = fields.Char(string="FPS ID/Mobile Number/Email Address")
|
||||
|
||||
@api.constrains('l10n_hk_fps_type', 'l10n_hk_fps_identifier')
|
||||
def _check_l10n_hk_fps_identifier(self):
|
||||
auto_mobn_re = re.compile(r"^[+]\d{1,3}-\d{6,12}$")
|
||||
for bank in self:
|
||||
if bank.country_code != 'HK':
|
||||
continue
|
||||
if bank.l10n_hk_fps_type == 'id' and (not bank.l10n_hk_fps_identifier or len(bank.l10n_hk_fps_identifier) not in [7, 9]):
|
||||
raise ValidationError(_("Invalid FPS ID! Please enter a valid FPS ID with length 7 or 9."))
|
||||
if bank.l10n_hk_fps_type == 'mobile' and (not bank.l10n_hk_fps_identifier or not auto_mobn_re.match(bank.l10n_hk_fps_identifier)):
|
||||
raise ValidationError(_("Invalid Mobile! Please enter a valid mobile number with format +852-67891234."))
|
||||
if bank.l10n_hk_fps_type == 'email' and (not bank.l10n_hk_fps_identifier or not single_email_re.match(bank.l10n_hk_fps_identifier)):
|
||||
raise ValidationError(_("Invalid Email! Please enter a valid email address."))
|
||||
|
||||
# Follow the documentation of FPS QR Code Standard [1]
|
||||
# [1]: https://www.hkma.gov.hk/media/eng/doc/key-functions/financial-infrastructure/infrastructure/retail-payment-initiatives/Common_QR_Code_Specification.pdf
|
||||
def _get_merchant_account_info(self):
|
||||
if self.country_code == 'HK':
|
||||
fps_type_mapping = {
|
||||
'id': '02',
|
||||
'mobile': '03',
|
||||
'email': '04'
|
||||
}
|
||||
fps_type_id = fps_type_mapping[self.l10n_hk_fps_type]
|
||||
fps_account = f'{fps_type_id}{len(self.l10n_hk_fps_identifier):02}{self.l10n_hk_fps_identifier}'
|
||||
return f'0012hk.com.hkicl{fps_account}'
|
||||
return super()._get_merchant_account_info()
|
||||
|
||||
def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency):
|
||||
if qr_method == 'emv_qr' and self.country_code == 'HK':
|
||||
if currency.name not in ['HKD', 'CNY']:
|
||||
return _("Can't generate a FPS QR code with currency other than HKD or CNY.")
|
||||
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 == 'HK' and (not self.l10n_hk_fps_type or not self.l10n_hk_fps_identifier):
|
||||
return _("The account receiving the payment must have a FPS type and a FPS identifier set.")
|
||||
|
||||
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
@@ -0,0 +1 @@
|
||||
from . import l10n_hk_test_emv_qr
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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 L10nHkTestEMVQRCode(AccountTestInvoicingCommon):
|
||||
""" Test the generation of the EMV QR Code on invoices """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref='l10n_hk.l10n_hk_chart_template'):
|
||||
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.hk').id,
|
||||
'city': 'HK',
|
||||
})
|
||||
|
||||
cls.acc_emv_hk = cls.env['res.partner.bank'].create({
|
||||
'acc_number': '123456789012345678',
|
||||
'partner_id': cls.company_data['company'].partner_id.id,
|
||||
'l10n_hk_fps_type': 'mobile',
|
||||
'l10n_hk_fps_identifier': '+852-67891234',
|
||||
})
|
||||
|
||||
cls.acc_emv_hk_without_fps_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.HKD').id,
|
||||
'partner_bank_id': cls.acc_emv_hk.id,
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'invoice_line_ids': [Command.create({'quantity': 1, 'price_unit': 100})],
|
||||
})
|
||||
|
||||
def l10n_hk_test_emv_qr_code_generation(self):
|
||||
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()
|
||||
|
||||
# Without company partner city should fail
|
||||
self.emv_qr_invoice.currency_id = self.env.ref('base.HKD')
|
||||
self.company_data['company'].partner_id.city = False
|
||||
with self.assertRaises(UserError, msg="Missing Merchant City."):
|
||||
self.emv_qr_invoice._generate_qr_code()
|
||||
|
||||
# Without fps infomation should fail
|
||||
self.company_data['company'].partner_id.city = 'HK'
|
||||
self.emv_qr_invoice.partner_bank_id = self.acc_emv_hk_without_fps_info
|
||||
with self.assertRaises(UserError, msg="The account receiving the payment must have a FPS type and a FPS identifier set."):
|
||||
self.emv_qr_invoice._generate_qr_code()
|
||||
|
||||
def l10n_hk_test_emv_qr_vals(self):
|
||||
self.emv_qr_invoice.qr_code_method = 'emv_qr'
|
||||
unstruct_ref = self.emv_qr_invoice.ref or self.emv_qr_invoice.name
|
||||
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 crc16 value
|
||||
crc16 = emv_qr_vals[-1]
|
||||
self.assertEqual(crc16, '3CA6')
|
||||
|
||||
# Check the whole qr code string
|
||||
qr_code_string = ''.join(emv_qr_vals)
|
||||
self.assertEqual(qr_code_string, '00020101021226330012hk.com.hkicl0313+852-678912345204000053033445405115.05802HK5914company_1_data6002HK63043CA6')
|
||||
@@ -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="acc_holder_name" position="after">
|
||||
<field name="country_code" invisible="1" />
|
||||
<field name="l10n_hk_fps_type" attrs="{'invisible': [('country_code', '!=', 'HK')]}" />
|
||||
<field name="l10n_hk_fps_identifier" placeholder="e.g. +852-67891234" attrs="{'invisible': ['|', ('country_code', '!=', 'HK'), ('l10n_hk_fps_type', '=', False)]}" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user