diff --git a/addons/l10n_ch/__init__.py b/addons/l10n_ch/__init__.py index 45936de37a4..bdf4299e889 100644 --- a/addons/l10n_ch/__init__.py +++ b/addons/l10n_ch/__init__.py @@ -3,6 +3,7 @@ from . import models from . import report +from . import wizard from odoo import api, SUPERUSER_ID diff --git a/addons/l10n_ch/__manifest__.py b/addons/l10n_ch/__manifest__.py index 3b62a7390d1..b01bca79fc3 100644 --- a/addons/l10n_ch/__manifest__.py +++ b/addons/l10n_ch/__manifest__.py @@ -9,7 +9,7 @@ 'description': """ Swiss localization ================== -This module defines a chart of account for Switzerland (Swiss PME/KMU 2015), taxes and enables the generation of ISR when you print an invoice or send it by mail. +This module defines a chart of account for Switzerland (Swiss PME/KMU 2015), taxes and enables the generation of ISR and QR-bill when you print an invoice or send it by mail. An ISR will be generated if you specify the information it needs : - The bank account you expect to be paid on must be set, and have a valid postal reference. @@ -18,11 +18,17 @@ An ISR will be generated if you specify the information it needs : - You must have set the postal references of your bank. - Your invoice must be in EUR or CHF (as ISRs do not accept other currencies) -The generation of the ISR is automatic if you meet the previous criteria. +A QR-bill will be generated if: + - The partner set on your invoice has a complete address (street, city, postal code and country) in Switzerland + - The option to generate the Swiss QR-code is selected on the invoice (done by default) + - A correct account number/QR IBAN is set on your bank journal + - (when using a QR-IBAN): the payment reference of the invoice is a QR-reference + +The generation of the ISR and QR-bill is automatic if you meet the previous criteria. Here is how it works: - - Printing the invoice will trigger the download of two files: the invoice, and its ISR - - Clicking the 'Send by mail' button will attach two files to your draft mail : the invoice, and the corresponding ISR. + - Printing the invoice will trigger the download of three files: the invoice, its ISR and its QR-bill + - Clicking the 'Send by mail' button will attach three files to your draft mail : the invoice, the ISR and the QR-bill. """, 'version': '11.0', 'category': 'Accounting/Localizations/Account Charts', diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py index 07f087e7292..e8f3a64a7c7 100644 --- a/addons/l10n_ch/models/account_invoice.py +++ b/addons/l10n_ch/models/account_invoice.py @@ -119,9 +119,7 @@ class AccountMove(models.Model): (3) 1: control digit for identification number and reference """ for record in self: - has_qriban = record.partner_bank_id and record.partner_bank_id._is_qr_iban() or False - isr_subscription = record.l10n_ch_isr_subscription - if (has_qriban or isr_subscription) and record.name: + if (record.partner_bank_id.l10n_ch_qr_iban or record.l10n_ch_isr_subscription) and record.name: id_number = record._get_isrb_id_number() if id_number: id_number = id_number.zfill(l10n_ch_ISR_ID_NUM_LENGTH) @@ -246,7 +244,7 @@ class AccountMove(models.Model): for inv in self: if inv.move_type == 'in_invoice' and inv.company_id.account_fiscal_country_id.code == "CH": partner_bank = inv.partner_bank_id - needs_isr_ref = partner_bank._is_qr_iban() or partner_bank._is_isr_issuer() + needs_isr_ref = partner_bank.l10n_ch_qr_iban or partner_bank._is_isr_issuer() if needs_isr_ref and not inv._has_isr_ref(): inv.l10n_ch_isr_needs_fixing = True continue diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py index d583a0b743f..5802739568d 100644 --- a/addons/l10n_ch/models/res_bank.py +++ b/addons/l10n_ch/models/res_bank.py @@ -4,9 +4,10 @@ import re from odoo import api, fields, models, _ +from odoo.addons.base.models.res_bank import sanitize_account_number +from odoo.addons.base_iban.models.res_partner_bank import normalize_iban, pretty_iban, validate_iban from odoo.exceptions import ValidationError from odoo.tools.misc import mod10r -from odoo.exceptions import UserError import werkzeug.urls @@ -37,6 +38,30 @@ def _is_l10n_ch_isr_issuer(account_ref, currency_code): return _is_l10n_ch_postal(account_ref) return False +def validate_qr_iban(qr_iban): + # Check first if it's a valid IBAN. + validate_iban(qr_iban) + + # We sanitize first so that _check_qr_iban_range() can extract correct IID from IBAN to validate it. + sanitized_qr_iban = sanitize_account_number(qr_iban) + + if sanitized_qr_iban[:2] != 'CH': + raise ValidationError(_("QR-IBAN numbers are only available in Switzerland.")) + + # Now, check if it's valid QR-IBAN (based on its IID). + if not check_qr_iban_range(sanitized_qr_iban): + raise ValidationError(_("QR-IBAN '%s' is invalid.") % qr_iban) + + return True + +def check_qr_iban_range(iban): + if not iban or len(iban) < 9: + return False + iid_start_index = 4 + iid_end_index = 8 + iid = iban[iid_start_index : iid_end_index+1] + return re.match(r'\d+', iid) and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only + class ResPartnerBank(models.Model): _inherit = 'res.partner.bank' @@ -49,6 +74,14 @@ class ResPartnerBank(models.Model): "your own account. The client number is mostly 6 numbers without -, while the postal account number can " "be e.g. 01-162-8") + l10n_ch_qr_iban = fields.Char(string='QR-IBAN', + compute='_compute_l10n_ch_qr_iban', + store=True, + readonly=False, + help="Put the QR-IBAN here for your own bank accounts. That way, you can " + "still use the main IBAN in the Account Number while you will see the " + "QR-IBAN for the barcode. ") + # fields to configure ISR payment slip generation l10n_ch_isr_subscription_chf = fields.Char(string='CHF ISR Subscription Number', help='The subscription number provided by the bank or Postfinance to identify the bank, used to generate ISR in CHF. eg. 01-162-8') l10n_ch_isr_subscription_eur = fields.Char(string='EUR ISR Subscription Number', help='The subscription number provided by the bank or Postfinance to identify the bank, used to generate ISR in EUR. eg. 03-162-5') @@ -104,6 +137,33 @@ class ResPartnerBank(models.Model): bank.sanitized_acc_number = bank.acc_number super(ResPartnerBank, self - postal_banks)._compute_sanitized_acc_number() + @api.depends('acc_number') + def _compute_l10n_ch_qr_iban(self): + for record in self: + try: + validate_qr_iban(record.acc_number) + valid_qr_iban = True + except ValidationError: + valid_qr_iban = False + + if valid_qr_iban: + record.l10n_ch_qr_iban = record.sanitized_acc_number + else: + record.l10n_ch_qr_iban = None + + @api.model + def create(self, vals): + if vals.get('l10n_ch_qr_iban'): + validate_qr_iban(vals['l10n_ch_qr_iban']) + vals['l10n_ch_qr_iban'] = pretty_iban(normalize_iban(vals['l10n_ch_qr_iban'])) + return super().create(vals) + + def write(self, vals): + if vals.get('l10n_ch_qr_iban'): + validate_qr_iban(vals['l10n_ch_qr_iban']) + vals['l10n_ch_qr_iban'] = pretty_iban(normalize_iban(vals['l10n_ch_qr_iban'])) + return super().write(vals) + @api.model def _get_supported_account_types(self): rslt = super(ResPartnerBank, self)._get_supported_account_types() @@ -197,10 +257,13 @@ class ResPartnerBank(models.Model): # just like ISR number for invoices) reference_type = 'NON' reference = '' - if self._is_qr_iban(): + acc_number = self.sanitized_acc_number + + if self.l10n_ch_qr_iban: # _check_for_qr_code_errors ensures we can't have a QR-IBAN without a QR-reference here reference_type = 'QRR' reference = structured_communication + acc_number = sanitize_account_number(self.l10n_ch_qr_iban) currency = currency or self.currency_id or self.company_id.currency_id @@ -208,7 +271,7 @@ class ResPartnerBank(models.Model): 'SPC', # QR Type '0200', # Version '1', # Coding Type - self.sanitized_acc_number, # IBAN + acc_number, # IBAN / QR-IBAN 'K', # Creditor Address Type (self.acc_holder_name or self.partner_id.name)[:70], # Creditor Name creditor_addr_1, # Creditor Address Line 1 @@ -248,24 +311,6 @@ class ResPartnerBank(models.Model): line_2 = partner.zip + ' ' + partner.city return line_1[:70], line_2[:70] - def _check_qr_iban_range(self, iban): - if not iban or len(iban) < 9: - return False - iid_start_index = 4 - iid_end_index = 8 - iid = iban[iid_start_index : iid_end_index+1] - return re.match('\d+', iid) \ - and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only - - def _is_qr_iban(self): - """ Tells whether or not this bank account has a QR-IBAN account number. - QR-IBANs are specific identifiers used in Switzerland as references in - QR-codes. They are formed like regular IBANs, but are actually something - different. - """ - return self.acc_type == 'iban' \ - and self._check_qr_iban_range(self.sanitized_acc_number) - @api.model def _is_qr_reference(self, reference): """ Checks whether the given reference is a QR-reference, i.e. it is @@ -300,7 +345,7 @@ class ResPartnerBank(models.Model): if debtor_partner and not _partner_fields_set(debtor_partner): return _("The partner the QR-code must have a complete postal address (street, zip, city and country).") - if self._is_qr_iban() and not self._is_qr_reference(structured_communication): + if self.l10n_ch_qr_iban and not self._is_qr_reference(structured_communication): return _("When using a QR-IBAN as the destination account of a QR-code, the payment reference must be a QR-reference.") return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication) diff --git a/addons/l10n_ch/report/swissqr_report.xml b/addons/l10n_ch/report/swissqr_report.xml index a2d39223ac0..1617a512d70 100644 --- a/addons/l10n_ch/report/swissqr_report.xml +++ b/addons/l10n_ch/report/swissqr_report.xml @@ -37,7 +37,9 @@
Account / Payable to
-
+ + +


@@ -45,7 +47,7 @@
- +
Reference

@@ -93,7 +95,9 @@
Account / Payable to
-
+ + +


@@ -101,14 +105,14 @@
- +
Reference

- +
Additional information
diff --git a/addons/l10n_ch/tests/test_swissqr.py b/addons/l10n_ch/tests/test_swissqr.py index 207f6e361ef..57208286241 100644 --- a/addons/l10n_ch/tests/test_swissqr.py +++ b/addons/l10n_ch/tests/test_swissqr.py @@ -165,7 +165,7 @@ class TestSwissQR(AccountTestInvoicingCommon): def test_swissQR_qriban(self): # Now use a proper QR-IBAN, we are good to print a QR Bill qriban_account = self.create_account(QR_IBAN) - self.assertTrue(qriban_account.acc_type, 'qr-iban') + self.assertTrue(qriban_account.l10n_ch_qr_iban) self.invoice1.partner_bank_id = qriban_account self.invoice1.action_post() self.swissqr_generated(self.invoice1, ref_type="QRR") diff --git a/addons/l10n_ch/views/res_bank_view.xml b/addons/l10n_ch/views/res_bank_view.xml index e5503543f5d..cf6cc3d45d8 100644 --- a/addons/l10n_ch/views/res_bank_view.xml +++ b/addons/l10n_ch/views/res_bank_view.xml @@ -7,6 +7,7 @@ +