Skip to content

Commit da6c902

Browse files
yvaucherjco-odoo
authored andcommitted
[IMP] l10n_ch: clearer validations for bank fields and vendor bills (ISR ref)
On res.partner.bank, we check that: - l10n_ch_postal contains a valid postal number - l10n_ch_isr_subscription_chf contains a valid ISR subscription number - l10n_ch_isr_subscription_eur contains a valid ISR subscription number ISR subscriptions numbers are postal numbers but starting with 01 or 03. Those codes are reserved to ISR issuance. When the bank account on a Vendor Bill is detected as an ISR Issuer, check the reference is actually an ISR. The 27 digits ISR Reference is error prone when typed by hand and an error at this stage would break the payment process later. This is required to avoid batch payment error with SEPA. We prefer using the pretty form xx-yyyyy-z of a postal account. The Swiss users will identify it more easily. We always want to auto fill the field l10n_ch_postal when possible from acc_number, which includes only 2 cases of filling acc_number: 1. a 9 position postal account number 2. an IBAN from PostFinance which includes clearing 09000 Original prs: Closes odoo#51645, odoo#51544, odoo#51560 closes odoo#54455 X-original-commit: f8a3ec4 Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com> Signed-off-by: Josse Colpaert <jco@openerp.com>
1 parent e532189 commit da6c902

File tree

9 files changed

+394
-31
lines changed

9 files changed

+394
-31
lines changed

addons/l10n_ch/i18n_extra/l10n_ch.pot

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
#
55
msgid ""
66
msgstr ""
7-
"Project-Id-Version: Odoo Server 13.0\n"
7+
"Project-Id-Version: Odoo Server 13.0+e\n"
88
"Report-Msgid-Bugs-To: \n"
9-
"POT-Creation-Date: 2020-05-28 09:52+0000\n"
10-
"PO-Revision-Date: 2020-06-17 10:00+0000\n"
9+
"POT-Creation-Date: 2020-07-09 16:03+0000\n"
10+
"PO-Revision-Date: 2020-07-09 16:03+0000\n"
1111
"Last-Translator: \n"
1212
"Language-Team: \n"
1313
"MIME-Version: 1.0\n"
@@ -336,17 +336,17 @@ msgstr ""
336336

337337
#. module: l10n_ch
338338
#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_swissqr_template
339-
msgid "<span class=\"title\">Additional information</span><br/>"
339+
msgid "<span class=\"title\">Account / Payable to</span><br/>"
340340
msgstr ""
341341

342342
#. module: l10n_ch
343343
#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_swissqr_template
344-
msgid "<span class=\"title\">Amount</span><br/>"
344+
msgid "<span class=\"title\">Additional information</span><br/>"
345345
msgstr ""
346346

347347
#. module: l10n_ch
348348
#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_swissqr_template
349-
msgid "<span class=\"title\">Account / Payable to</span><br/>"
349+
msgid "<span class=\"title\">Amount</span><br/>"
350350
msgstr ""
351351

352352
#. module: l10n_ch
@@ -359,11 +359,6 @@ msgstr ""
359359
msgid "<span class=\"title\">Payable by</span><br/>"
360360
msgstr ""
361361

362-
#. module: l10n_ch
363-
#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_swissqr_template
364-
msgid "<span class=\"title\">Payable to</span><br/>"
365-
msgstr ""
366-
367362
#. module: l10n_ch
368363
#: model_terms:ir.ui.view,arch_db:l10n_ch.l10n_ch_swissqr_template
369364
msgid "<span class=\"title\">Reference</span><br/>"
@@ -1027,7 +1022,6 @@ msgid "Immeubles d’exploitation"
10271022
msgstr ""
10281023

10291024
#. module: l10n_ch
1030-
#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_import
10311025
#: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_import
10321026
msgid "Import/Export"
10331027
msgstr ""
@@ -1080,6 +1074,11 @@ msgstr ""
10801074
msgid "Journal Entries"
10811075
msgstr ""
10821076

1077+
#. module: l10n_ch
1078+
#: model:ir.model.fields,field_description:l10n_ch.field_account_move__l10n_ch_isr_needs_fixing
1079+
msgid "L10N Ch Isr Needs Fixing"
1080+
msgstr ""
1081+
10831082
#. module: l10n_ch
10841083
#: model:ir.model.fields,field_description:l10n_ch.field_account_move__l10n_ch_isr_number
10851084
msgid "L10N Ch Isr Number"
@@ -1216,6 +1215,13 @@ msgstr ""
12161215
msgid "Plan comptable 2015 (Suisse)"
12171216
msgstr ""
12181217

1218+
#. module: l10n_ch
1219+
#: model_terms:ir.ui.view,arch_db:l10n_ch.isr_invoice_form
1220+
msgid ""
1221+
"Please fill in a correct ISR reference in the payment reference. The banks "
1222+
"will refuse your payment file otherwise."
1223+
msgstr ""
1224+
12191225
#. module: l10n_ch
12201226
#: code:addons/l10n_ch/models/res_bank.py:0
12211227
#, python-format
@@ -1419,7 +1425,6 @@ msgid "Subventions, taxes touristiques à 0%"
14191425
msgstr ""
14201426

14211427
#. module: l10n_ch
1422-
#: model:account.fiscal.position,name:l10n_ch.1_fiscal_position_template_1
14231428
#: model:account.fiscal.position.template,name:l10n_ch.fiscal_position_template_1
14241429
msgid "Suisse national"
14251430
msgstr ""
@@ -1576,11 +1581,27 @@ msgstr ""
15761581
msgid "TVA due à 7.7% (Incl. TN)"
15771582
msgstr ""
15781583

1584+
#. module: l10n_ch
1585+
#: code:addons/l10n_ch/models/res_bank.py:0
1586+
#, python-format
1587+
msgid ""
1588+
"The ISR subcription {} for {} number is not valid.\n"
1589+
"It must starts with {} and we a valid postal number format. eg. {}"
1590+
msgstr ""
1591+
15791592
#. module: l10n_ch
15801593
#: model:ir.model.fields,help:l10n_ch.field_account_move__l10n_ch_currency_name
15811594
msgid "The name of this invoice's currency"
15821595
msgstr ""
15831596

1597+
#. module: l10n_ch
1598+
#: code:addons/l10n_ch/models/res_bank.py:0
1599+
#, python-format
1600+
msgid ""
1601+
"The postal number {} is not valid.\n"
1602+
"It must be a valid postal number format. eg. 10-8060-7"
1603+
msgstr ""
1604+
15841605
#. module: l10n_ch
15851606
#: model:ir.model.fields,help:l10n_ch.field_account_move__l10n_ch_isr_number
15861607
msgid "The reference number associated with this invoice"
@@ -1627,6 +1648,13 @@ msgstr ""
16271648
msgid "Travaux en cours"
16281649
msgstr ""
16291650

1651+
#. module: l10n_ch
1652+
#: model:ir.model.fields,help:l10n_ch.field_account_move__l10n_ch_isr_needs_fixing
1653+
msgid ""
1654+
"Used to show a warning banner when the vendor bill needs a correct ISR "
1655+
"payment reference. "
1656+
msgstr ""
1657+
16301658
#. module: l10n_ch
16311659
#: model:account.account.template,name:l10n_ch.ch_coa_3940
16321660
msgid "Variation de la valeur des prestations non facturées"

addons/l10n_ch/models/account_invoice.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class AccountMove(models.Model):
2727

2828
l10n_ch_isr_sent = fields.Boolean(default=False, help="Boolean value telling whether or not the ISR corresponding to this invoice has already been printed or sent by mail.")
2929
l10n_ch_currency_name = fields.Char(related='currency_id.name', readonly=True, string="Currency Name", help="The name of this invoice's currency") #This field is used in the "invisible" condition field of the 'Print ISR' button.
30+
l10n_ch_isr_needs_fixing = fields.Boolean(compute="_compute_l10n_ch_isr_needs_fixing", help="Used to show a warning banner when the vendor bill needs a correct ISR payment reference. ")
3031

3132
@api.depends('partner_bank_id.l10n_ch_isr_subscription_eur', 'partner_bank_id.l10n_ch_isr_subscription_chf')
3233
def _compute_l10n_ch_isr_subscription(self):
@@ -154,6 +155,33 @@ def _compute_l10n_ch_isr_valid(self):
154155
record.l10n_ch_isr_subscription and \
155156
record.l10n_ch_currency_name in ['EUR', 'CHF']
156157

158+
@api.depends('move_type', 'partner_bank_id', 'payment_reference')
159+
def _compute_l10n_ch_isr_needs_fixing(self):
160+
for inv in self:
161+
if inv.move_type == 'in_invoice' and inv.company_id.country_id.code == "CH":
162+
partner_bank = inv.partner_bank_id
163+
if partner_bank._is_isr_issuer() and not inv._has_isr_ref():
164+
inv.l10n_ch_isr_needs_fixing = True
165+
continue
166+
inv.l10n_ch_isr_needs_fixing = False
167+
168+
def _has_isr_ref(self):
169+
"""Check if this invoice has a valid ISR reference (for Switzerland)
170+
e.g.
171+
12371
172+
000000000000000000000012371
173+
210000000003139471430009017
174+
21 00000 00003 13947 14300 09017
175+
"""
176+
self.ensure_one()
177+
ref = self.payment_reference or self.ref
178+
if not ref:
179+
return False
180+
ref = ref.replace(' ', '')
181+
if re.match(r'^(\d{2,27})$', ref):
182+
return ref == mod10r(ref[:-1])
183+
return False
184+
157185
def split_total_amount(self):
158186
""" Splits the total amount of this invoice in two parts, using the dot as
159187
a separator, and taking two precision digits (always displayed).
@@ -173,7 +201,7 @@ def isr_print(self):
173201
self.l10n_ch_isr_sent = True
174202
return self.env.ref('l10n_ch.l10n_ch_isr_report').report_action(self)
175203
else:
176-
raise ValidationError(_("""You cannot generate an ISR yet.\n
204+
raise ValidationError(_("""You cannot generate an ISR yet.\n
177205
For this, you need to :\n
178206
- set a valid postal account number (or an IBAN referencing one) for your company\n
179207
- define its bank\n

addons/l10n_ch/models/res_bank.py

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,39 @@
44
import re
55

66
from odoo import api, fields, models, _
7+
from odoo.exceptions import ValidationError
78
from odoo.tools.misc import mod10r
89
from odoo.exceptions import UserError
910

1011
import werkzeug.urls
1112

13+
ISR_SUBSCRIPTION_CODE = {'CHF': '01', 'EUR': '03'}
14+
CLEARING = "09000"
15+
_re_postal = re.compile('^[0-9]{2}-[0-9]{1,6}-[0-9]$')
16+
17+
1218
def _is_l10n_ch_postal(account_ref):
13-
""" Returns True iff the string account_ref is a valid postal account number,
19+
""" Returns True if the string account_ref is a valid postal account number,
1420
i.e. it only contains ciphers and is last cipher is the result of a recursive
1521
modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
1622
"""
17-
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', account_ref or ''):
23+
if _re_postal.match(account_ref or ''):
1824
ref_subparts = account_ref.split('-')
19-
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6,'0') + ref_subparts[2]
25+
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6, '0') + ref_subparts[2]
2026

2127
if re.match('\d+$', account_ref or ''):
2228
account_ref_without_check = account_ref[:-1]
2329
return mod10r(account_ref_without_check) == account_ref
2430
return False
2531

32+
def _is_l10n_ch_isr_issuer(account_ref, currency_code):
33+
""" Returns True if the string account_ref is a valid a valid ISR issuer
34+
An ISR issuer is postal account number that starts by 01 (CHF) or 03 (EUR),
35+
"""
36+
if (account_ref or '').startswith(ISR_SUBSCRIPTION_CODE[currency_code]):
37+
return _is_l10n_ch_postal(account_ref)
38+
return False
39+
2640

2741
class ResPartnerBank(models.Model):
2842
_inherit = 'res.partner.bank'
@@ -40,6 +54,38 @@ class ResPartnerBank(models.Model):
4054
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')
4155
l10n_ch_show_subscription = fields.Boolean(compute='_compute_l10n_ch_show_subscription', default=lambda self: self.env.company.country_id.code == 'CH')
4256

57+
def _is_isr_issuer(self):
58+
return (_is_l10n_ch_isr_issuer(self.l10n_ch_postal, 'CHF')
59+
or _is_l10n_ch_isr_issuer(self.l10n_ch_postal, 'EUR'))
60+
61+
@api.constrains("l10n_ch_postal", "partner_id")
62+
def _check_postal_num(self):
63+
"""Validate postal number format"""
64+
for rec in self:
65+
if rec.l10n_ch_postal and not _is_l10n_ch_postal(self.l10n_ch_postal):
66+
# l10n_ch_postal is used for the purpose of Client Number on your own accounts, so don't do the check there
67+
if rec.partner_id and not rec.partner_id.ref_company_ids:
68+
raise ValidationError(
69+
_("The postal number {} is not valid.\n"
70+
"It must be a valid postal number format. eg. 10-8060-7").format(rec.l10n_ch_postal))
71+
return True
72+
73+
@api.constrains("l10n_ch_isr_subscription_chf", "l10n_ch_isr_subscription_eur")
74+
def _check_subscription_num(self):
75+
"""Validate ISR subscription number format
76+
Subscription number can only starts with 01 or 03
77+
"""
78+
for rec in self:
79+
for currency in ["CHF", "EUR"]:
80+
subscrip = rec.l10n_ch_isr_subscription_chf if currency == "CHF" else rec.l10n_ch_isr_subscription_eur
81+
if subscrip and not _is_l10n_ch_isr_issuer(subscrip, currency):
82+
example = "01-162-8" if currency == "CHF" else "03-162-5"
83+
raise ValidationError(
84+
_("The ISR subcription {} for {} number is not valid.\n"
85+
"It must starts with {} and we a valid postal number format. eg. {}"
86+
).format(subscrip, currency, ISR_SUBSCRIPTION_CODE[currency], example))
87+
return True
88+
4389
@api.depends('partner_id', 'company_id')
4490
def _compute_l10n_ch_show_subscription(self):
4591
for bank in self:
@@ -89,19 +135,45 @@ def _compute_l10n_ch_postal(self):
89135
record.l10n_ch_postal = record.acc_number.split(" ")[0]
90136
else:
91137
record.l10n_ch_postal = record.acc_number
92-
if record.partner_id:
93-
record.acc_number = record.acc_number + ' ' + record.partner_id.name
138+
# In case of ISR issuer, this number is not
139+
# unique and we fill acc_number with partner
140+
# name to give proper information to the user
141+
if record.partner_id and record.acc_number[:2] in ["01", "03"]:
142+
record.acc_number = ("{} {}").format(record.acc_number, record.partner_id.name)
143+
144+
@api.model
145+
def _is_postfinance_iban(self, iban):
146+
"""Postfinance IBAN have format
147+
CHXX 0900 0XXX XXXX XXXX K
148+
Where 09000 is the clearing number
149+
"""
150+
return iban.startswith('CH') and iban[4:9] == CLEARING
151+
152+
@api.model
153+
def _pretty_postal_num(self, number):
154+
"""format a postal account number or an ISR subscription number
155+
as per specifications with '-' separators.
156+
eg. 010001628 -> 01-162-8
157+
"""
158+
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', number or ''):
159+
return number
160+
currency_code = number[:2]
161+
middle_part = number[2:-1]
162+
trailing_cipher = number[-1]
163+
middle_part = middle_part.lstrip("0")
164+
return currency_code + '-' + middle_part + '-' + trailing_cipher
94165

95166
@api.model
96167
def _retrieve_l10n_ch_postal(self, iban):
97-
""" Reads a swiss postal account number from a an IBAN and returns it as
168+
"""Reads a swiss postal account number from a an IBAN and returns it as
98169
a string. Returns None if no valid postal account number was found, or
99-
the given iban was not from Switzerland.
170+
the given iban was not from Swiss Postfinance.
171+
172+
CH09 0900 0000 1000 8060 7 -> 10-8060-7
100173
"""
101-
if iban[:2] == 'CH':
102-
#the IBAN corresponds to a swiss account
103-
if _is_l10n_ch_postal(iban[-12:]):
104-
return iban[-12:]
174+
if self._is_postfinance_iban(iban):
175+
# the IBAN corresponds to a swiss account
176+
return self._pretty_postal_num(iban[-9:])
105177
return None
106178

107179
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):

addons/l10n_ch/tests/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

44
from . import test_ch_qr_code
5-
from . import test_l10n_ch_isr
65
from . import test_swissqr
6+
from . import test_l10n_ch_isr_print
7+
from . import test_vendor_bill_isr
8+
from . import test_onchange_l10n_ch_postal

addons/l10n_ch/tests/test_l10n_ch_isr.py renamed to addons/l10n_ch/tests/test_l10n_ch_isr_print.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def assertBankAccountValid(account_number, expected_account_type, expected_posta
3434

3535
assertBankAccountValid('010391391', 'postal', expected_postal='010391391')
3636
assertBankAccountValid('010391394', 'bank')
37-
assertBankAccountValid('CH6309000000250097798', 'iban', expected_postal='000250097798')
37+
assertBankAccountValid('CH6309000000250097798', 'iban', expected_postal='25-9779-8')
3838
assertBankAccountValid('GR1601101250000000012300695', 'iban', expected_postal=False)
3939

4040
def test_isr(self):

0 commit comments

Comments
 (0)