diff --git a/addons/l10n_hk/__manifest__.py b/addons/l10n_hk/__manifest__.py index b9067aeca44..c4b23a41b6b 100644 --- a/addons/l10n_hk/__manifest__.py +++ b/addons/l10n_hk/__manifest__.py @@ -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', diff --git a/addons/l10n_hk/i18n/l10n_hk.pot b/addons/l10n_hk/i18n/l10n_hk.pot new file mode 100644 index 00000000000..0f0bf4aec5d --- /dev/null +++ b/addons/l10n_hk/i18n/l10n_hk.pot @@ -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 "" diff --git a/addons/l10n_hk/models/__init__.py b/addons/l10n_hk/models/__init__.py index 4e8c82f884b..6ed6ddefad1 100644 --- a/addons/l10n_hk/models/__init__.py +++ b/addons/l10n_hk/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_hk diff --git a/addons/l10n_hk/models/res_bank.py b/addons/l10n_hk/models/res_bank.py new file mode 100644 index 00000000000..51cb68153f9 --- /dev/null +++ b/addons/l10n_hk/models/res_bank.py @@ -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) diff --git a/addons/l10n_hk/tests/__init__.py b/addons/l10n_hk/tests/__init__.py new file mode 100644 index 00000000000..0432debc912 --- /dev/null +++ b/addons/l10n_hk/tests/__init__.py @@ -0,0 +1 @@ +from . import l10n_hk_test_emv_qr diff --git a/addons/l10n_hk/tests/l10n_hk_test_emv_qr.py b/addons/l10n_hk/tests/l10n_hk_test_emv_qr.py new file mode 100644 index 00000000000..832a07f8732 --- /dev/null +++ b/addons/l10n_hk/tests/l10n_hk_test_emv_qr.py @@ -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') 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..852e8b5ca42 --- /dev/null +++ b/addons/l10n_hk/views/res_bank_views.xml @@ -0,0 +1,17 @@ + + + + + res.partner.bank.form.inherit + res.partner.bank + + + + + + + + + + +