From 799e0ff84cb9e91ca28286fdb51902b86bbfa08a Mon Sep 17 00:00:00 2001 From: "Tommy (tong)" Date: Fri, 19 May 2023 06:12:05 +0000 Subject: [PATCH] [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 --- addons/account_qr_code_emv/__manifest__.py | 3 + addons/account_qr_code_emv/models/res_bank.py | 59 ++++++++++++------- .../views/res_bank_views.xml | 21 +++++++ addons/l10n_hk/__manifest__.py | 1 - addons/l10n_hk/models/res_bank.py | 59 +++++++++++-------- addons/l10n_hk/tests/test_l10n_hk_emv_qr.py | 5 +- addons/l10n_hk/views/res_bank_views.xml | 17 ------ 7 files changed, 101 insertions(+), 64 deletions(-) create mode 100644 addons/account_qr_code_emv/views/res_bank_views.xml delete mode 100644 addons/l10n_hk/views/res_bank_views.xml diff --git a/addons/account_qr_code_emv/__manifest__.py b/addons/account_qr_code_emv/__manifest__.py index 4fc1201e551..5bdb39b3291 100644 --- a/addons/account_qr_code_emv/__manifest__.py +++ b/addons/account_qr_code_emv/__manifest__.py @@ -9,5 +9,8 @@ """, 'author': 'Odoo SA', 'depends': ['account'], + 'data': [ + 'views/res_bank_views.xml', + ], 'license': 'LGPL-3', } diff --git a/addons/account_qr_code_emv/models/res_bank.py b/addons/account_qr_code_emv/models/res_bank.py index b0865716eb8..bd7f8fec056 100644 --- a/addons/account_qr_code_emv/models/res_bank.py +++ b/addons/account_qr_code_emv/models/res_bank.py @@ -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 diff --git a/addons/account_qr_code_emv/views/res_bank_views.xml b/addons/account_qr_code_emv/views/res_bank_views.xml new file mode 100644 index 00000000000..e79bbf5fb7f --- /dev/null +++ b/addons/account_qr_code_emv/views/res_bank_views.xml @@ -0,0 +1,21 @@ + + + + + res.partner.bank.form.inherit + res.partner.bank + + + + + + + + + + + + + + + diff --git a/addons/l10n_hk/__manifest__.py b/addons/l10n_hk/__manifest__.py index c4b23a41b6b..ef7f5fbb038 100644 --- a/addons/l10n_hk/__manifest__.py +++ b/addons/l10n_hk/__manifest__.py @@ -12,7 +12,6 @@ ], '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 56861fc5eb9..41401f265c4 100644 --- a/addons/l10n_hk/models/res_bank.py +++ b/addons/l10n_hk/models/res_bank.py @@ -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) 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 708e5225342..d1cb59ff32e 100644 --- a/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py +++ b/addons/l10n_hk/tests/test_l10n_hk_emv_qr.py @@ -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({ diff --git a/addons/l10n_hk/views/res_bank_views.xml b/addons/l10n_hk/views/res_bank_views.xml deleted file mode 100644 index 852e8b5ca42..00000000000 --- a/addons/l10n_hk/views/res_bank_views.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - res.partner.bank.form.inherit - res.partner.bank - - - - - - - - - - -