[IMP] account_qr_code_emv,l10n_hk: code refractoring for reusing
Impacted versions: - saas-16.4 or above This commit improve below features: - Move country_code to account_qr_code_emv - Add _serialize method to compute the qr_code_vals - Fix additional data field should return if country_code mismatched - Merge fields l10n_hk_fps_type to proxy_type - Merge fields l10n_hk_fps_identifier to proxy_value - Add include_reference boolean to res.partner.bank Part-of: odoo/odoo#122802
This commit is contained in:
@@ -9,5 +9,8 @@
|
||||
""",
|
||||
'author': 'Odoo SA',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'views/res_bank_views.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import re
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
from odoo.addons.account_qr_code_emv.const import CURRENCY_MAPPING
|
||||
|
||||
@@ -10,6 +10,23 @@ from odoo.addons.account_qr_code_emv.const import CURRENCY_MAPPING
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
country_code = fields.Char(related='partner_id.country_code', string="Country Code")
|
||||
display_qr_setting = fields.Boolean(compute='_compute_display_qr_setting')
|
||||
include_reference = fields.Boolean(string="Include Reference", help="Include the reference in the QR code.")
|
||||
proxy_type = fields.Selection([('none', 'None')], string="Proxy Type", default='none')
|
||||
proxy_value = fields.Char(string="Proxy Value")
|
||||
|
||||
@api.model
|
||||
def _serialize(self, header, value):
|
||||
if value is not None and value != '':
|
||||
return f'{header:02}{len(str(value)):02}{value}'
|
||||
else:
|
||||
return ''
|
||||
|
||||
@api.depends('country_code')
|
||||
def _compute_display_qr_setting(self):
|
||||
self.display_qr_setting = False
|
||||
|
||||
# CRC16 calculation with polynomial 0x1021 and initial value 0xFFFF
|
||||
def _get_crc16(self, data, poly=0x1021, init=0xFFFF):
|
||||
crc = init
|
||||
@@ -23,10 +40,10 @@ class ResPartnerBank(models.Model):
|
||||
return crc & 0xFFFF
|
||||
|
||||
def _get_merchant_account_info(self):
|
||||
return False
|
||||
return None
|
||||
|
||||
def _get_additional_data_field(self, comment):
|
||||
return False
|
||||
return None
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'emv_qr':
|
||||
@@ -36,27 +53,27 @@ class ResPartnerBank(models.Model):
|
||||
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)
|
||||
additional_data_field = self._get_additional_data_field(comment) if self.include_reference else None
|
||||
|
||||
qr_code_vals = [
|
||||
'000201', # Payload Format Indicator
|
||||
'010212', # Dynamic QR Codes
|
||||
f'26{len(merchant_account_info):02}{merchant_account_info}', # Merchant Account Information
|
||||
'52040000', # Merchant Category Code
|
||||
f'5303{currency_code}', # Transaction Currency
|
||||
f'54{len(str(amount)):02}{amount}', # Transaction Amount
|
||||
f'5802{self.partner_id.country_code}', # Country Code
|
||||
f'59{len(merchant_name):02}{merchant_name}', # Merchant Name
|
||||
f'60{len(merchant_city):02}{merchant_city}', # Merchant City
|
||||
(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
|
||||
]
|
||||
|
||||
if additional_data_field:
|
||||
qr_code_vals.append(f'62{len(additional_data_field):02}{additional_data_field}') # Additional Data Field
|
||||
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'))
|
||||
qr_code_str += format(crc, '04x').upper()
|
||||
return qr_code_str
|
||||
|
||||
qr_code_vals.append('6304') # CRC16
|
||||
crc = self._get_crc16(bytes(''.join(qr_code_vals), 'utf-8'))
|
||||
qr_code_vals.append(format(crc, '04x').upper())
|
||||
return qr_code_vals
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
@@ -66,7 +83,7 @@ class ResPartnerBank(models.Model):
|
||||
'width': 128,
|
||||
'height': 128,
|
||||
'humanreadable': 1,
|
||||
'value': ''.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
|
||||
'value': self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication),
|
||||
}
|
||||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
@@ -76,6 +93,8 @@ 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.")
|
||||
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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">
|
||||
<sheet position="inside">
|
||||
<field name="display_qr_setting" invisible="1" />
|
||||
<group string="EMV QR Configuration" attrs="{'invisible': [('display_qr_setting', '=', False)]}">
|
||||
<field name="country_code" invisible="1" />
|
||||
<field name="proxy_type"/>
|
||||
<field name="proxy_value"/>
|
||||
<field name="include_reference"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -12,7 +12,6 @@
|
||||
],
|
||||
'data': [
|
||||
'data/account_chart_template_data.xml',
|
||||
'views/res_bank_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo_company.xml',
|
||||
|
||||
@@ -10,50 +10,61 @@ 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 Identifier", help="FPS ID, Mobile Number or Email Address")
|
||||
proxy_type = fields.Selection(selection_add=[('id', "FPS ID"), ('mobile', "Mobile Number"), ('email', "Email Address")],
|
||||
ondelete={'id': 'set default', 'mobile': 'set default', 'email': 'set default'})
|
||||
|
||||
@api.constrains('l10n_hk_fps_type', 'l10n_hk_fps_identifier')
|
||||
def _check_l10n_hk_fps_identifier(self):
|
||||
@api.constrains('proxy_type', 'proxy_value', 'partner_id')
|
||||
def _check_hk_proxy(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."))
|
||||
for bank in self.filtered(lambda b: b.country_code == 'HK'):
|
||||
if bank.proxy_type not in ['id', 'mobile', 'email', 'none', False]:
|
||||
raise ValidationError(_("The FPS Type must be either ID, Mobile or Email to generate a FPS QR code for account number %s.", bank.acc_number))
|
||||
if bank.proxy_type == 'id' and (not bank.proxy_value or len(bank.proxy_value) not in [7, 9]):
|
||||
raise ValidationError(_("Invalid FPS ID! Please enter a valid FPS ID with length 7 or 9 for account number %s.", bank.acc_number))
|
||||
if bank.proxy_type == 'mobile' and (not bank.proxy_value or not auto_mobn_re.match(bank.proxy_value)):
|
||||
raise ValidationError(_("Invalid Mobile! Please enter a valid mobile number with format +852-67891234 for account number %s.", bank.acc_number))
|
||||
if bank.proxy_type == 'email' and (not bank.proxy_value or not single_email_re.match(bank.proxy_value)):
|
||||
raise ValidationError(_("Invalid Email! Please enter a valid email address for account number %s.", bank.acc_number))
|
||||
|
||||
@api.depends('country_code')
|
||||
def _compute_display_qr_setting(self):
|
||||
bank_hk = self.filtered(lambda b: b.country_code == 'HK')
|
||||
bank_hk.display_qr_setting = self.env.company.qr_code
|
||||
super(ResPartnerBank, self - bank_hk)._compute_display_qr_setting()
|
||||
|
||||
# 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'
|
||||
'id': 2,
|
||||
'mobile': 3,
|
||||
'email': 4,
|
||||
}
|
||||
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}'
|
||||
fps_type = fps_type_mapping[self.proxy_type]
|
||||
merchant_account_vals = [
|
||||
(0, 'hk.com.hkicl'), # GUID
|
||||
(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 super()._get_merchant_account_info()
|
||||
|
||||
def _get_additional_data_field(self, comment):
|
||||
return f'05{len(comment):02}{comment}'
|
||||
if self.country_code == 'HK':
|
||||
return self._serialize(5, 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 == 'HK':
|
||||
if currency.name not in ['HKD', 'CNY']:
|
||||
return _("Can't generate a FPS QR code with currency other than HKD or CNY.")
|
||||
return _("Can't generate a FPS QR code with a 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.")
|
||||
if qr_method == 'emv_qr' and self.country_code == 'HK' and self.proxy_type not in ['id', 'mobile', 'email']:
|
||||
return _("The FPS Type must be either ID, Mobile or Email to generate a FPS QR code.")
|
||||
|
||||
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
@@ -27,8 +27,9 @@ class TestL10nHKEmvQrCode(AccountTestInvoicingCommon):
|
||||
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',
|
||||
'proxy_type': 'mobile',
|
||||
'proxy_value': '+852-67891234',
|
||||
'include_reference': True,
|
||||
})
|
||||
|
||||
cls.acc_emv_hk_without_fps_info = cls.env['res.partner.bank'].create({
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?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