From 2af6b7dcf6c151532c5900f41dcd00efebdb3b05 Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Thu, 21 May 2020 00:08:11 +0000 Subject: [PATCH] [REF] l10n_ch ISR number computation Add hooks to be able to tweak the Bank customer ID number for ISR-B Those hooks will allow to not use of the field l10n_ch_postal for multiple purposes. Fixes conditions on which the reference is generated. It shouldn't be always generated if l10n_ch_postal is set. Only if a l10n_ch_subscription_xxx is set. closes odoo/odoo#58398 X-original-commit: 4790b8e57aa357cda32818392d795b8c0dc48f43 Signed-off-by: oco-odoo Signed-off-by: Christophe Monniez (moc) --- addons/l10n_ch/models/account_invoice.py | 67 ++++++++----- addons/l10n_ch/tests/__init__.py | 1 + .../l10n_ch/tests/test_gen_isr_reference.py | 98 +++++++++++++++++++ 3 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 addons/l10n_ch/tests/test_gen_isr_reference.py diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py index f7ca87c2895..d952a168bf9 100644 --- a/addons/l10n_ch/models/account_invoice.py +++ b/addons/l10n_ch/models/account_invoice.py @@ -10,7 +10,7 @@ from odoo.tools.misc import mod10r l10n_ch_ISR_NUMBER_LENGTH = 27 -l10n_ch_ISR_NUMBER_ISSUER_LENGTH = 12 +l10n_ch_ISR_ID_NUM_LENGTH = 6 class AccountMove(models.Model): _inherit = 'account.move' @@ -61,7 +61,18 @@ class AccountMove(models.Model): record.l10n_ch_isr_subscription = _format_isr_subscription_scanline(isr_subscription) record.l10n_ch_isr_subscription_formatted = _format_isr_subscription(isr_subscription) - @api.depends('name', 'partner_bank_id.l10n_ch_postal', 'partner_bank_id.acc_number') + def _get_isrb_id_number(self): + """Hook to fix the lack of proper field for ISR-B Customer ID""" + # FIXME + # replace l10n_ch_postal by an other field to not mix ISR-B + # customer ID as it forbid the following validations on l10n_ch_postal + # number for Vendor bank accounts: + # - validation of format xx-yyyyy-c + # - validation of checksum + self.ensure_one() + return self.partner_bank_id.l10n_ch_postal or '' + + @api.depends('name', 'partner_bank_id.l10n_ch_postal') def _compute_l10n_ch_isr_number(self): """Generates the ISR or QRR reference @@ -109,14 +120,20 @@ class AccountMove(models.Model): """ for record in self: has_qriban = record.partner_bank_id and record.partner_bank_id._is_qr_iban() or False - isr_subscription = (record.partner_bank_id.l10n_ch_postal or '').replace("-", "") # In case the user put the - + isr_subscription = record.l10n_ch_isr_subscription if (has_qriban or isr_subscription) and record.name: - invoice_issuer_ref = (isr_subscription or '').ljust(l10n_ch_ISR_NUMBER_ISSUER_LENGTH, '0') + id_number = record._get_isrb_id_number() + if id_number: + id_number = id_number.zfill(l10n_ch_ISR_ID_NUM_LENGTH) invoice_ref = re.sub('[^\d]', '', record.name) - #We only keep the last digits of the sequence number if it is too long - invoice_ref = invoice_ref[-l10n_ch_ISR_NUMBER_ISSUER_LENGTH:] - internal_ref = invoice_ref.zfill(l10n_ch_ISR_NUMBER_LENGTH - l10n_ch_ISR_NUMBER_ISSUER_LENGTH - 1) # -1 for mod10r check character - record.l10n_ch_isr_number = mod10r(invoice_issuer_ref + internal_ref) + # keep only the last digits if it exceed boundaries + full_len = len(id_number) + len(invoice_ref) + ref_payload_len = l10n_ch_ISR_NUMBER_LENGTH - 1 + extra = full_len - ref_payload_len + if extra > 0: + invoice_ref = invoice_ref[extra:] + internal_ref = invoice_ref.zfill(ref_payload_len - len(id_number)) + record.l10n_ch_isr_number = mod10r(id_number + internal_ref) else: record.l10n_ch_isr_number = False @@ -138,10 +155,23 @@ class AccountMove(models.Model): else: record.l10n_ch_isr_number_spaced = False + def _get_l10n_ch_isr_optical_amount(self): + """Prepare amount string for ISR optical line""" + self.ensure_one() + currency_code = None + if self.currency_id.name == 'CHF': + currency_code = '01' + elif self.currency_id.name == 'EUR': + currency_code = '03' + units, cents = float_split_str(self.amount_residual, 2) + amount_to_display = units + cents + amount_ref = amount_to_display.zfill(10) + optical_amount = currency_code + amount_ref + optical_amount = mod10r(optical_amount) + return optical_amount @api.depends( 'currency_id.name', 'amount_residual', 'name', - 'partner_bank_id.l10n_ch_postal', 'partner_bank_id.l10n_ch_isr_subscription_eur', 'partner_bank_id.l10n_ch_isr_subscription_chf') def _compute_l10n_ch_isr_optical_line(self): @@ -192,19 +222,12 @@ class AccountMove(models.Model): for record in self: record.l10n_ch_isr_optical_line = '' if record.l10n_ch_isr_number and record.l10n_ch_isr_subscription and record.currency_id.name: - #Left part - currency_code = None - if record.currency_id.name == 'CHF': - currency_code = '01' - elif record.currency_id.name == 'EUR': - currency_code = '03' - units, cents = float_split_str(record.amount_residual, 2) - amount_to_display = units + cents - amount_ref = amount_to_display.zfill(10) - left = currency_code + amount_ref - left = mod10r(left) - #Final assembly (the space after the '+' is no typo, it stands in the specs.) - record.l10n_ch_isr_optical_line = left + '>' + record.l10n_ch_isr_number + '+ ' + record.l10n_ch_isr_subscription + '>' + # Final assembly (the space after the '+' is no typo, it stands in the specs.) + record.l10n_ch_isr_optical_line = '{amount}>{reference}+ {creditor}>'.format( + amount=record._get_l10n_ch_isr_optical_amount(), + reference=record.l10n_ch_isr_number, + creditor=record.l10n_ch_isr_subscription, + ) @api.depends( 'move_type', 'name', 'currency_id.name', diff --git a/addons/l10n_ch/tests/__init__.py b/addons/l10n_ch/tests/__init__.py index 65de010232a..e9792e787e3 100644 --- a/addons/l10n_ch/tests/__init__.py +++ b/addons/l10n_ch/tests/__init__.py @@ -6,3 +6,4 @@ from . import test_swissqr from . import test_l10n_ch_isr_print from . import test_vendor_bill_isr from . import test_onchange_l10n_ch_postal +from . import test_gen_isr_reference diff --git a/addons/l10n_ch/tests/test_gen_isr_reference.py b/addons/l10n_ch/tests/test_gen_isr_reference.py new file mode 100644 index 00000000000..6d8a75dd083 --- /dev/null +++ b/addons/l10n_ch/tests/test_gen_isr_reference.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import common, Form + +QR_IBAN = 'CH21 3080 8001 2345 6782 7' +ISR_SUBS_NUMBER = "01-162-8" + + +class TestGenISRReference(AccountTestInvoicingCommon): + """Check condition of generation of and content of the structured ref""" + + @classmethod + def setUpClass(cls, chart_template_ref="l10n_ch.l10nch_chart_template"): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.bank = cls.env["res.bank"].create( + { + "name": "Alternative Bank Schweiz AG", + "bic": "ALSWCH21XXX", + } + ) + cls.bank_acc_isr = cls.env["res.partner.bank"].create( + { + "acc_number": "ISR", + "l10n_ch_isr_subscription_chf": "01-162-8", + "bank_id": cls.bank.id, + "partner_id": cls.partner_a.id, + } + ) + cls.bank_acc_qriban = cls.env["res.partner.bank"].create( + { + "acc_number": QR_IBAN, + "bank_id": cls.bank.id, + "partner_id": cls.partner_a.id, + } + ) + cls.invoice = cls.init_invoice("out_invoice", products=cls.product_a+cls.product_b) + + def test_isr(self): + + self.invoice.partner_bank_id = self.bank_acc_isr + self.invoice.name = "INV/01234567890" + + expected_isr = "000000000000000012345678903" + expected_optical_line = "0100001307807>000000000000000012345678903+ 010001628>" + self.assertEqual(self.invoice.l10n_ch_isr_number, expected_isr) + self.assertEqual(self.invoice.l10n_ch_isr_optical_line, expected_optical_line) + + def test_qrr(self): + self.invoice.partner_bank_id = self.bank_acc_qriban + + self.invoice.name = "INV/01234567890" + + expected_isr = "000000000000000012345678903" + self.assertEqual(self.invoice.l10n_ch_isr_number, expected_isr) + # No need to check optical line, we have no use for it with QR-bill + + def test_isr_long_reference(self): + self.invoice.partner_bank_id = self.bank_acc_isr + + self.invoice.name = "INV/123456789012345678901234567890" + + expected_isr = "567890123456789012345678901" + expected_optical_line = "0100001307807>567890123456789012345678901+ 010001628>" + self.assertEqual(self.invoice.l10n_ch_isr_number, expected_isr) + self.assertEqual(self.invoice.l10n_ch_isr_optical_line, expected_optical_line) + + def test_missing_isr_subscription_num(self): + self.bank_acc_isr.l10n_ch_isr_subscription_chf = False + + self.invoice.partner_bank_id = self.bank_acc_isr + + self.assertFalse(self.invoice.l10n_ch_isr_number) + self.assertFalse(self.invoice.l10n_ch_isr_optical_line) + + def test_missing_isr_subscription_num_in_wrong_field(self): + self.bank_acc_isr.l10n_ch_isr_subscription_chf = False + self.bank_acc_isr.l10n_ch_postal = ISR_SUBS_NUMBER + + self.invoice.partner_bank_id = self.bank_acc_isr + + self.assertFalse(self.invoice.l10n_ch_isr_number) + self.assertFalse(self.invoice.l10n_ch_isr_optical_line) + + def test_no_bank_account(self): + self.invoice.partner_bank_id = False + + self.assertFalse(self.invoice.l10n_ch_isr_number) + self.assertFalse(self.invoice.l10n_ch_isr_optical_line) + + def test_wrong_currency(self): + self.invoice.partner_bank_id = self.bank_acc_isr + self.invoice.currency_id = self.env.ref("base.BTN") + + self.assertFalse(self.invoice.l10n_ch_isr_number) + self.assertFalse(self.invoice.l10n_ch_isr_optical_line)