mirror of
https://github.com/bringout/oca-ocb-l10n_europe.git
synced 2026-04-26 22:22:00 +02:00
19.0 vanilla
This commit is contained in:
parent
ff721d030e
commit
7721452493
1826 changed files with 124775 additions and 274114 deletions
|
|
@ -1,11 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_config_settings
|
||||
from . import template_ch
|
||||
from . import account_invoice
|
||||
from . import account_journal
|
||||
from . import res_bank
|
||||
from . import res_company
|
||||
from . import account_bank_statement
|
||||
from . import ir_actions_report
|
||||
from . import chart_template
|
||||
from . import account_payment
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.addons.l10n_ch.models.res_bank import _is_l10n_ch_postal
|
||||
from odoo.tools.misc import str2bool
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
|
||||
_inherit = "account.bank.statement.line"
|
||||
|
||||
def _find_or_create_bank_account(self):
|
||||
if self.company_id.account_fiscal_country_id.code in ('CH', 'LI') and _is_l10n_ch_postal(self.account_number):
|
||||
bank_account = self.env['res.partner.bank'].search(
|
||||
[('company_id', '=', self.company_id.id),
|
||||
('sanitized_acc_number', 'like', self.account_number + '%'),
|
||||
('partner_id', '=', self.partner_id.id)])
|
||||
if not bank_account and not str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param("account.skip_create_bank_account_on_reconcile")
|
||||
):
|
||||
bank_account = self.env['res.partner.bank'].create({
|
||||
'company_id': self.company_id.id,
|
||||
'acc_number': self.account_number + " " + self.partner_id.name,
|
||||
'partner_id': self.partner_id.id
|
||||
})
|
||||
return bank_account
|
||||
else:
|
||||
return super()._find_or_create_bank_account()
|
||||
|
|
@ -4,342 +4,74 @@ import re
|
|||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.float_utils import float_split_str
|
||||
from odoo.tools.misc import mod10r
|
||||
|
||||
l10n_ch_ISR_NUMBER_LENGTH = 27
|
||||
l10n_ch_ISR_ID_NUM_LENGTH = 6
|
||||
L10N_CH_QRR_NUMBER_LENGTH = 27
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
# NOTE
|
||||
# The ISR system is kept and taken into account up to September 2022.
|
||||
# After that, the transition to the QR system will be completed and the ISR system won't exist anymore.
|
||||
# This means that Odoo v16 shouldn't support the ISR system and all the references to it should be cleaned up by then.
|
||||
# In the versions leading to that change,
|
||||
# although the functions related to the ISR are still taken into account and still exist,
|
||||
# the QR billing is always preferred.
|
||||
|
||||
_inherit = 'account.move'
|
||||
|
||||
l10n_ch_isr_subscription = fields.Char(compute='_compute_l10n_ch_isr_subscription', help='ISR subscription number identifying your company or your bank to generate ISR.')
|
||||
l10n_ch_isr_subscription_formatted = fields.Char(compute='_compute_l10n_ch_isr_subscription', help="ISR subscription number your company or your bank, formated with '-' and without the padding zeros, to generate ISR report.")
|
||||
|
||||
l10n_ch_isr_number = fields.Char(compute='_compute_l10n_ch_isr_number', store=True, help='The reference number associated with this invoice')
|
||||
l10n_ch_isr_number_spaced = fields.Char(compute='_compute_l10n_ch_isr_number_spaced', help="ISR number split in blocks of 5 characters (right-justified), to generate ISR report.")
|
||||
|
||||
l10n_ch_isr_optical_line = fields.Char(compute="_compute_l10n_ch_isr_optical_line", help='Optical reading line, as it will be printed on ISR')
|
||||
|
||||
l10n_ch_isr_valid = fields.Boolean(compute='_compute_l10n_ch_isr_valid', help='Boolean value. True iff all the data required to generate the ISR are present')
|
||||
|
||||
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.")
|
||||
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.
|
||||
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. ")
|
||||
|
||||
l10n_ch_is_qr_valid = fields.Boolean(compute='_compute_l10n_ch_qr_is_valid', help="Determines whether an invoice can be printed as a QR or not")
|
||||
|
||||
@api.depends('partner_id', 'currency_id')
|
||||
def _compute_l10n_ch_qr_is_valid(self):
|
||||
for move in self:
|
||||
company_eligible = True
|
||||
|
||||
if(move.company_id.account_fiscal_country_id.code != 'CH'):
|
||||
company_eligible = False
|
||||
|
||||
if move.partner_bank_id.acc_number and move.partner_bank_id.acc_type == 'iban':
|
||||
iban = move.partner_bank_id.acc_number.replace(' ', '')
|
||||
if iban.startswith('CH') and len(iban) >= 9:
|
||||
bank_code = iban[4:9]
|
||||
if bank_code.isdigit() and 30000 <= int(bank_code) <= 31999:
|
||||
company_eligible = True
|
||||
|
||||
error_messages = move.partner_bank_id._get_error_messages_for_qr('ch_qr', move.partner_id, move.currency_id)
|
||||
move.l10n_ch_is_qr_valid = (
|
||||
move.move_type == 'out_invoice'
|
||||
and move.partner_bank_id._eligible_for_qr_code('ch_qr', move.partner_id, move.currency_id, raises_error=False)
|
||||
and company_eligible
|
||||
move.move_type == 'out_invoice' and
|
||||
not error_messages and
|
||||
(
|
||||
# QR codes must be printed on all Swiss transactions
|
||||
move.company_id.account_fiscal_country_id.code == 'CH' or
|
||||
(
|
||||
# QR code is also printed if the fiscal country is not Switzerland but the receivale account is eligible
|
||||
move.partner_bank_id.acc_type == 'iban' and
|
||||
(iban := (move.partner_bank_id.acc_number or '').replace(' ', '')).startswith('CH') and
|
||||
iban[4:9].isdigit() and
|
||||
30000 <= int(iban[4:9]) <= 31999
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends('partner_bank_id.l10n_ch_isr_subscription_eur', 'partner_bank_id.l10n_ch_isr_subscription_chf')
|
||||
def _compute_l10n_ch_isr_subscription(self):
|
||||
""" Computes the ISR subscription identifying your company or the bank that allows to generate ISR. And formats it accordingly"""
|
||||
def _format_isr_subscription(isr_subscription):
|
||||
#format the isr as per specifications
|
||||
currency_code = isr_subscription[:2]
|
||||
middle_part = isr_subscription[2:-1]
|
||||
trailing_cipher = isr_subscription[-1]
|
||||
middle_part = re.sub('^0*', '', middle_part)
|
||||
return currency_code + '-' + middle_part + '-' + trailing_cipher
|
||||
|
||||
def _format_isr_subscription_scanline(isr_subscription):
|
||||
# format the isr for scanline
|
||||
return isr_subscription[:2] + isr_subscription[2:-1].rjust(6, '0') + isr_subscription[-1:]
|
||||
|
||||
for record in self:
|
||||
record.l10n_ch_isr_subscription = False
|
||||
record.l10n_ch_isr_subscription_formatted = False
|
||||
if record.partner_bank_id:
|
||||
if record.currency_id.name == 'EUR':
|
||||
isr_subscription = record.partner_bank_id.l10n_ch_isr_subscription_eur
|
||||
elif record.currency_id.name == 'CHF':
|
||||
isr_subscription = record.partner_bank_id.l10n_ch_isr_subscription_chf
|
||||
else:
|
||||
#we don't format if in another currency as EUR or CHF
|
||||
continue
|
||||
|
||||
if isr_subscription:
|
||||
isr_subscription = isr_subscription.replace("-", "") # In case the user put the -
|
||||
record.l10n_ch_isr_subscription = _format_isr_subscription_scanline(isr_subscription)
|
||||
record.l10n_ch_isr_subscription_formatted = _format_isr_subscription(isr_subscription)
|
||||
|
||||
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
|
||||
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):
|
||||
for record in self:
|
||||
if (record.partner_bank_id.l10n_ch_qr_iban or record.l10n_ch_isr_subscription) and record.name:
|
||||
invoice_ref = re.sub(r'\D', '', record.name)
|
||||
record.l10n_ch_isr_number = record._compute_isr_number(invoice_ref)
|
||||
else:
|
||||
record.l10n_ch_isr_number = False
|
||||
|
||||
@api.model
|
||||
def _compute_isr_number(self, invoice_ref):
|
||||
r"""Generates the ISR or QRR reference
|
||||
|
||||
An ISR references are 27 characters long.
|
||||
QRR is a recycling of ISR for QR-bills. Thus works the same.
|
||||
def get_l10n_ch_qrr_number(self):
|
||||
"""Generates the QRR reference.
|
||||
QRR references are 27 characters long.
|
||||
|
||||
The invoice sequence number is used, removing each of its non-digit characters,
|
||||
and pad the unused spaces on the left of this number with zeros.
|
||||
The last digit is a checksum (mod10r).
|
||||
|
||||
There are 2 types of references:
|
||||
|
||||
* ISR (Postfinance)
|
||||
|
||||
The reference is free but for the last
|
||||
digit which is a checksum.
|
||||
If shorter than 27 digits, it is filled with zeros on the left.
|
||||
|
||||
e.g.
|
||||
|
||||
120000000000234478943216899
|
||||
\________________________/|
|
||||
1 2
|
||||
(1) 12000000000023447894321689 | reference
|
||||
(2) 9: control digit for identification number and reference
|
||||
|
||||
* ISR-B (Indirect through a bank, requires a customer ID)
|
||||
|
||||
In case of ISR-B The firsts digits (usually 6), contain the customer ID
|
||||
at the Bank of this ISR's issuer.
|
||||
The rest (usually 20 digits) is reserved for the reference plus the
|
||||
control digit.
|
||||
If the [customer ID] + [the reference] + [the control digit] is shorter
|
||||
than 27 digits, it is filled with zeros between the customer ID till
|
||||
the start of the reference.
|
||||
|
||||
e.g.
|
||||
|
||||
150001123456789012345678901
|
||||
\____/\__________________/|
|
||||
1 2 3
|
||||
(1) 150001 | id number of the customer (size may vary)
|
||||
(2) 12345678901234567890 | reference
|
||||
(3) 1: control digit for identification number and reference
|
||||
"""
|
||||
id_number = self._get_isrb_id_number()
|
||||
if id_number:
|
||||
id_number = id_number.zfill(l10n_ch_ISR_ID_NUM_LENGTH)
|
||||
self.ensure_one()
|
||||
if self.partner_bank_id.l10n_ch_qr_iban and self.l10n_ch_is_qr_valid and self.name:
|
||||
invoice_ref = re.sub(r'[^\d]', '', self.name)
|
||||
return self._compute_qrr_number(invoice_ref)
|
||||
else:
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _compute_qrr_number(self, invoice_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
|
||||
ref_payload_len = L10N_CH_QRR_NUMBER_LENGTH - 1
|
||||
extra = len(invoice_ref) - ref_payload_len
|
||||
if extra > 0:
|
||||
invoice_ref = invoice_ref[extra:]
|
||||
internal_ref = invoice_ref.zfill(ref_payload_len - len(id_number))
|
||||
|
||||
return mod10r(id_number + internal_ref)
|
||||
|
||||
@api.depends('l10n_ch_isr_number')
|
||||
def _compute_l10n_ch_isr_number_spaced(self):
|
||||
def _space_isr_number(isr_number):
|
||||
to_treat = isr_number
|
||||
res = ''
|
||||
while to_treat:
|
||||
res = to_treat[-5:] + res
|
||||
to_treat = to_treat[:-5]
|
||||
if to_treat:
|
||||
res = ' ' + res
|
||||
return res
|
||||
|
||||
for record in self:
|
||||
if record.l10n_ch_isr_number:
|
||||
record.l10n_ch_isr_number_spaced = _space_isr_number(record.l10n_ch_isr_number)
|
||||
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_isr_subscription_eur',
|
||||
'partner_bank_id.l10n_ch_isr_subscription_chf')
|
||||
def _compute_l10n_ch_isr_optical_line(self):
|
||||
r""" Compute the optical line to print on the bottom of the ISR.
|
||||
|
||||
This line is read by an OCR.
|
||||
It's format is:
|
||||
|
||||
amount>reference+ creditor>
|
||||
|
||||
Where:
|
||||
|
||||
- amount: currency and invoice amount
|
||||
- reference: ISR structured reference number
|
||||
- in case of ISR-B contains the Customer ID number
|
||||
- it can also contains a partner reference (of the debitor)
|
||||
- creditor: Subscription number of the creditor
|
||||
|
||||
An optical line can have the 2 following formats:
|
||||
|
||||
* ISR (Postfinance)
|
||||
|
||||
0100003949753>120000000000234478943216899+ 010001628>
|
||||
|/\________/| \________________________/| \_______/
|
||||
1 2 3 4 5 6
|
||||
|
||||
(1) 01 | currency
|
||||
(2) 0000394975 | amount 3949.75
|
||||
(3) 4 | control digit for amount
|
||||
(5) 12000000000023447894321689 | reference
|
||||
(6) 9: control digit for identification number and reference
|
||||
(7) 010001628: subscription number (01-162-8)
|
||||
|
||||
* ISR-B (Indirect through a bank, requires a customer ID)
|
||||
|
||||
0100000494004>150001123456789012345678901+ 010234567>
|
||||
|/\________/| \____/\__________________/| \_______/
|
||||
1 2 3 4 5 6 7
|
||||
|
||||
(1) 01 | currency
|
||||
(2) 0000049400 | amount 494.00
|
||||
(3) 4 | control digit for amount
|
||||
(4) 150001 | id number of the customer (size may vary, usually 6 chars)
|
||||
(5) 12345678901234567890 | reference
|
||||
(6) 1: control digit for identification number and reference
|
||||
(7) 010234567: subscription number (01-23456-7)
|
||||
"""
|
||||
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:
|
||||
# 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',
|
||||
'partner_bank_id.l10n_ch_isr_subscription_eur',
|
||||
'partner_bank_id.l10n_ch_isr_subscription_chf')
|
||||
def _compute_l10n_ch_isr_valid(self):
|
||||
"""Returns True if all the data required to generate the ISR are present"""
|
||||
for record in self:
|
||||
record.l10n_ch_isr_valid = record.move_type == 'out_invoice' and\
|
||||
record.name and \
|
||||
record.l10n_ch_isr_subscription and \
|
||||
record.l10n_ch_currency_name in ['EUR', 'CHF']
|
||||
|
||||
@api.depends('move_type', 'partner_bank_id', 'payment_reference')
|
||||
def _compute_l10n_ch_isr_needs_fixing(self):
|
||||
for inv in self:
|
||||
if inv.move_type == 'in_invoice' and inv.company_id.account_fiscal_country_id.code in ('CH', 'LI'):
|
||||
partner_bank = inv.partner_bank_id
|
||||
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
|
||||
inv.l10n_ch_isr_needs_fixing = False
|
||||
|
||||
def _has_isr_ref(self):
|
||||
"""Check if this invoice has a valid ISR reference (for Switzerland)
|
||||
e.g.
|
||||
12371
|
||||
000000000000000000000012371
|
||||
210000000003139471430009017
|
||||
21 00000 00003 13947 14300 09017
|
||||
"""
|
||||
self.ensure_one()
|
||||
ref = self.payment_reference or self.ref
|
||||
if not ref:
|
||||
return False
|
||||
ref = ref.replace(' ', '')
|
||||
if re.match(r'^(\d{2,27})$', ref):
|
||||
return ref == mod10r(ref[:-1])
|
||||
return False
|
||||
|
||||
def split_total_amount(self):
|
||||
""" Splits the total amount of this invoice in two parts, using the dot as
|
||||
a separator, and taking two precision digits (always displayed).
|
||||
These two parts are returned as the two elements of a tuple, as strings
|
||||
to print in the report.
|
||||
|
||||
This function is needed on the model, as it must be called in the report
|
||||
template, which cannot reference static functions
|
||||
"""
|
||||
return float_split_str(self.amount_residual, 2)
|
||||
|
||||
def action_invoice_sent(self):
|
||||
# OVERRIDE
|
||||
rslt = super(AccountMove, self).action_invoice_sent()
|
||||
if self.l10n_ch_isr_valid or self.l10n_ch_is_qr_valid:
|
||||
rslt['context']['l10n_ch_mark_isr_as_sent'] = True
|
||||
return rslt
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
if self.env.context.get('l10n_ch_mark_isr_as_sent'):
|
||||
self.filtered(lambda inv: not inv.l10n_ch_isr_sent).write({'l10n_ch_isr_sent': True})
|
||||
return super(AccountMove, self.with_context(mail_post_autofollow=self.env.context.get('mail_post_autofollow', True))).message_post(**kwargs)
|
||||
internal_ref = invoice_ref.zfill(ref_payload_len)
|
||||
return mod10r(internal_ref)
|
||||
|
||||
def _get_invoice_reference_ch_invoice(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
""" This sets QRR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
# l10n_ch_isr_number is not always computed at this stage, and could change value when the invoice is posted.
|
||||
# We manually compute here it to avoid this conflict.
|
||||
self._compute_l10n_ch_isr_number()
|
||||
return self.l10n_ch_isr_number
|
||||
return self.get_l10n_ch_qrr_number()
|
||||
|
||||
def _get_invoice_reference_ch_partner(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
""" This sets QRR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number
|
||||
return self.get_l10n_ch_qrr_number()
|
||||
|
||||
@api.model
|
||||
def space_qrr_reference(self, qrr_ref):
|
||||
|
|
@ -358,7 +90,6 @@ class AccountMove(models.Model):
|
|||
""" Makes the provided SCOR reference human-friendly, spacing its elements
|
||||
by blocks of 5 from right to left.
|
||||
"""
|
||||
|
||||
return ' '.join(iso11649_ref[i:i + 4] for i in range(0, len(iso11649_ref), 4))
|
||||
|
||||
def l10n_ch_action_print_qr(self):
|
||||
|
|
@ -374,7 +105,6 @@ class AccountMove(models.Model):
|
|||
'name': (_("Some invoices could not be printed in the QR format")),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'l10n_ch.qr_invoice.wizard',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_ids': self.ids},
|
||||
|
|
@ -383,9 +113,7 @@ class AccountMove(models.Model):
|
|||
|
||||
def _l10n_ch_dispatch_invoices_to_print(self):
|
||||
qr_invs = self.filtered('l10n_ch_is_qr_valid')
|
||||
isr_invs = self.filtered('l10n_ch_isr_valid')
|
||||
return {
|
||||
'qr': qr_invs,
|
||||
'isr': isr_invs,
|
||||
'classic': self - qr_invs - isr_invs,
|
||||
'classic': self - qr_invs,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@ class AccountJournal(models.Model):
|
|||
_inherit = 'account.journal'
|
||||
|
||||
invoice_reference_model = fields.Selection(selection_add=[
|
||||
('ch', 'Switzerland')
|
||||
('ch', 'Switzerland (12 34560 00103 88500 1000 19188)')
|
||||
], ondelete={'ch': lambda recs: recs.write({'invoice_reference_model': 'odoo'})})
|
||||
|
||||
def _process_reference_for_sale_order(self, order_reference):
|
||||
'''
|
||||
returns the order reference to be used for the payment respecting the ISR
|
||||
Returns the order reference to be used for the payment, respecting the QRR standard.
|
||||
'''
|
||||
self.ensure_one()
|
||||
if self.invoice_reference_model == 'ch':
|
||||
# converting the sale order name into a unique number. Letters are converted to their base10 value
|
||||
invoice_ref = "".join([a if a.isdigit() else str(ord(a)) for a in order_reference])
|
||||
# id_number = self.company_id.bank_ids.l10n_ch_postal or ''
|
||||
order_reference = self.env['account.move']._compute_isr_number(invoice_ref)
|
||||
order_reference = self.env['account.move']._compute_qrr_number(invoice_ref)
|
||||
return order_reference
|
||||
return super()._process_reference_for_sale_order(order_reference)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
from odoo import _, models, fields, api
|
||||
from odoo.tools import mod10r
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = "account.payment"
|
||||
|
||||
l10n_ch_reference_warning_msg = fields.Char(compute='_compute_l10n_ch_reference_warning_msg')
|
||||
|
||||
@api.onchange('partner_id', 'memo', 'payment_type')
|
||||
def _compute_l10n_ch_reference_warning_msg(self):
|
||||
for payment in self:
|
||||
if payment.payment_type == 'outbound' and\
|
||||
payment.partner_id.country_code in ['CH', 'LI'] and\
|
||||
payment.partner_bank_id.l10n_ch_qr_iban and\
|
||||
not payment._l10n_ch_reference_is_valid(payment.memo):
|
||||
payment.l10n_ch_reference_warning_msg = _("Please fill in a correct QRR reference in the payment reference. The banks will refuse your payment file otherwise.")
|
||||
else:
|
||||
payment.l10n_ch_reference_warning_msg = False
|
||||
|
||||
def _l10n_ch_reference_is_valid(self, payment_reference):
|
||||
"""Check if this invoice has a valid reference (for Switzerland)
|
||||
e.g.
|
||||
000000000000000000000012371
|
||||
210000000003139471430009017
|
||||
21 00000 00003 13947 14300 09017
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not payment_reference:
|
||||
return False
|
||||
ref = payment_reference.replace(' ', '')
|
||||
if re.match(r'^(\d{2,27})$', ref):
|
||||
return ref == mod10r(ref[:-1])
|
||||
return False
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
# Write paperformat and report template used on company
|
||||
def _load(self, company):
|
||||
res = super(AccountChartTemplate, self)._load(company)
|
||||
if self == self.env.ref('l10n_ch.l10nch_chart_template'):
|
||||
company.write({
|
||||
'external_report_layout_id': self.env.ref('l10n_din5008.external_layout_din5008').id,
|
||||
'paperformat_id': self.env.ref('l10n_din5008.paperformat_euro_din').id
|
||||
})
|
||||
return res
|
||||
|
|
@ -10,6 +10,7 @@ from reportlab.lib.units import mm
|
|||
CH_QR_CROSS_SIZE_RATIO = 0.1522 # Ratio between the side length of the Swiss QR-code cross image and the QR-code's
|
||||
CH_QR_CROSS_FILE = Path('../static/src/img/CH-Cross_7mm.png') # Image file containing the Swiss QR-code cross to add on top of the QR-code
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = 'ir.actions.report'
|
||||
|
||||
|
|
@ -38,73 +39,39 @@ class IrActionsReport(models.Model):
|
|||
report = self._get_report(report_ref)
|
||||
if self._is_invoice_report(report_ref):
|
||||
invoices = self.env[report.model].browse(res_ids)
|
||||
# Determine which invoices need a QR/ISR.
|
||||
qr_inv_ids = []
|
||||
isr_inv_ids = []
|
||||
for invoice in invoices:
|
||||
# avoid duplicating existing streams
|
||||
if report.attachment_use and report.retrieve_attachment(invoice):
|
||||
continue
|
||||
if invoice.l10n_ch_is_qr_valid:
|
||||
qr_inv_ids.append(invoice.id)
|
||||
elif invoice.company_id.country_code == 'CH' and invoice.l10n_ch_isr_valid:
|
||||
isr_inv_ids.append(invoice.id)
|
||||
# Render the additional reports.
|
||||
streams_to_append = {}
|
||||
|
||||
# Determine which invoices need a QR.
|
||||
qr_inv_ids = invoices.filtered('l10n_ch_is_qr_valid').ids
|
||||
|
||||
if qr_inv_ids:
|
||||
qr_res = self._render_qweb_pdf_prepare_streams(
|
||||
'l10n_ch.l10n_ch_qr_report',
|
||||
{
|
||||
**data,
|
||||
'skip_headers': False,
|
||||
},
|
||||
data,
|
||||
res_ids=qr_inv_ids,
|
||||
)
|
||||
header = self.env.ref('l10n_ch.l10n_ch_qr_header', raise_if_not_found=False)
|
||||
if header:
|
||||
# Make a separated rendering to get the a page containing the company header. Then, merge the qr bill with it.
|
||||
|
||||
header_res = self._render_qweb_pdf_prepare_streams(
|
||||
'l10n_ch.l10n_ch_qr_header',
|
||||
{
|
||||
**data,
|
||||
'skip_headers': True,
|
||||
},
|
||||
res_ids=qr_inv_ids,
|
||||
)
|
||||
for invoice_id, stream in qr_res.items():
|
||||
qr_pdf = OdooPdfFileReader(stream['stream'], strict=False)
|
||||
res_pdf = OdooPdfFileReader(res[invoice_id]['stream'], strict=False)
|
||||
|
||||
for invoice_id, stream in qr_res.items():
|
||||
qr_pdf = OdooPdfFileReader(stream['stream'], strict=False)
|
||||
header_pdf = OdooPdfFileReader(header_res[invoice_id]['stream'], strict=False)
|
||||
last_page = res_pdf.getPage(-1)
|
||||
last_page.mergePage(qr_pdf.getPage(0))
|
||||
|
||||
page = header_pdf.getPage(0)
|
||||
page.mergePage(qr_pdf.getPage(0))
|
||||
output_pdf = OdooPdfFileWriter()
|
||||
|
||||
output_pdf = OdooPdfFileWriter()
|
||||
output_pdf.addPage(page)
|
||||
new_pdf_stream = io.BytesIO()
|
||||
output_pdf.write(new_pdf_stream)
|
||||
streams_to_append[invoice_id] = {'stream': new_pdf_stream}
|
||||
else:
|
||||
for invoice_id, stream in qr_res.items():
|
||||
streams_to_append[invoice_id] = stream
|
||||
# Add all pages from the original PDF except the last one
|
||||
for page_num in range(res_pdf.getNumPages() - 1):
|
||||
output_pdf.addPage(res_pdf.getPage(page_num))
|
||||
|
||||
if isr_inv_ids:
|
||||
isr_res = self._render_qweb_pdf_prepare_streams('l10n_ch.l10n_ch_isr_report', data, res_ids=isr_inv_ids)
|
||||
for invoice_id, stream in isr_res.items():
|
||||
streams_to_append[invoice_id] = stream
|
||||
output_pdf.addPage(last_page) # Add the modified last page (with the QR code merged)
|
||||
|
||||
new_pdf_stream = io.BytesIO()
|
||||
output_pdf.write(new_pdf_stream)
|
||||
new_pdf_stream.seek(0)
|
||||
res[invoice_id]['stream'].close()
|
||||
res[invoice_id]['stream'] = new_pdf_stream
|
||||
stream['stream'].close()
|
||||
|
||||
# Add to results
|
||||
for invoice_id, additional_stream in streams_to_append.items():
|
||||
invoice_stream = res[invoice_id]['stream']
|
||||
writer = OdooPdfFileWriter()
|
||||
writer.appendPagesFromReader(OdooPdfFileReader(invoice_stream, strict=False))
|
||||
writer.appendPagesFromReader(OdooPdfFileReader(additional_stream['stream'], strict=False))
|
||||
new_pdf_stream = io.BytesIO()
|
||||
writer.write(new_pdf_stream)
|
||||
res[invoice_id]['stream'] = new_pdf_stream
|
||||
invoice_stream.close()
|
||||
additional_stream['stream'].close()
|
||||
return res
|
||||
|
||||
def get_paperformat(self):
|
||||
|
|
|
|||
|
|
@ -6,37 +6,14 @@ from stdnum.util import clean
|
|||
|
||||
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, UserError
|
||||
from odoo.addons.base_iban.models.res_partner_bank import normalize_iban, pretty_iban, validate_iban, get_iban_part
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import LazyTranslate, street_split
|
||||
from odoo.tools.misc import mod10r
|
||||
|
||||
ISR_SUBSCRIPTION_CODE = {'CHF': '01', 'EUR': '03'}
|
||||
CLEARING = "09000"
|
||||
_re_postal = re.compile('^[0-9]{2}-[0-9]{1,6}-[0-9]$')
|
||||
_lt = LazyTranslate(__name__)
|
||||
|
||||
|
||||
def _is_l10n_ch_postal(account_ref):
|
||||
""" Returns True if the string account_ref is a valid postal account number,
|
||||
i.e. it only contains ciphers and is last cipher is the result of a recursive
|
||||
modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
|
||||
"""
|
||||
if _re_postal.match(account_ref or ''):
|
||||
ref_subparts = account_ref.split('-')
|
||||
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6, '0') + ref_subparts[2]
|
||||
|
||||
if re.match(r'\d+$', account_ref or ''):
|
||||
account_ref_without_check = account_ref[:-1]
|
||||
return mod10r(account_ref_without_check) == account_ref
|
||||
return False
|
||||
|
||||
def _is_l10n_ch_isr_issuer(account_ref, currency_code):
|
||||
""" Returns True if the string account_ref is a valid a valid ISR issuer
|
||||
An ISR issuer is postal account number that starts by 01 (CHF) or 03 (EUR),
|
||||
"""
|
||||
if (account_ref or '').startswith(ISR_SUBSCRIPTION_CODE[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)
|
||||
|
|
@ -45,34 +22,24 @@ def validate_qr_iban(qr_iban):
|
|||
sanitized_qr_iban = sanitize_account_number(qr_iban)
|
||||
|
||||
if sanitized_qr_iban[:2] not in ['CH', 'LI']:
|
||||
raise ValidationError(_("QR-IBAN numbers are only available in Switzerland."))
|
||||
raise ValidationError(_lt("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)
|
||||
raise ValidationError(_lt("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]
|
||||
iid = get_iban_part(iban, 'bank')
|
||||
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'
|
||||
|
||||
l10n_ch_postal = fields.Char(
|
||||
string="Swiss Postal Account",
|
||||
readonly=False, store=True,
|
||||
compute='_compute_l10n_ch_postal',
|
||||
help="This field is used for the Swiss postal account number on a vendor account and for the client number on "
|
||||
"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,
|
||||
|
|
@ -81,60 +48,18 @@ class ResPartnerBank(models.Model):
|
|||
"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')
|
||||
l10n_ch_show_subscription = fields.Boolean(compute='_compute_l10n_ch_show_subscription', default=lambda self: self.env.company.account_fiscal_country_id.code == 'CH')
|
||||
|
||||
def _is_isr_issuer(self):
|
||||
return (_is_l10n_ch_isr_issuer(self.l10n_ch_postal, 'CHF')
|
||||
or _is_l10n_ch_isr_issuer(self.l10n_ch_postal, 'EUR'))
|
||||
|
||||
@api.constrains("l10n_ch_postal", "partner_id")
|
||||
def _check_postal_num(self):
|
||||
"""Validate postal number format"""
|
||||
for rec in self:
|
||||
if rec.l10n_ch_postal and not _is_l10n_ch_postal(rec.l10n_ch_postal):
|
||||
# l10n_ch_postal is used for the purpose of Client Number on your own accounts, so don't do the check there
|
||||
if rec.partner_id and not rec.partner_id.ref_company_ids:
|
||||
raise ValidationError(
|
||||
_("The postal number {} is not valid.\n"
|
||||
"It must be a valid postal number format. eg. 10-8060-7").format(rec.l10n_ch_postal))
|
||||
return True
|
||||
|
||||
@api.constrains("l10n_ch_isr_subscription_chf", "l10n_ch_isr_subscription_eur")
|
||||
def _check_subscription_num(self):
|
||||
"""Validate ISR subscription number format
|
||||
Subscription number can only starts with 01 or 03
|
||||
"""
|
||||
for rec in self:
|
||||
for currency in ["CHF", "EUR"]:
|
||||
subscrip = rec.l10n_ch_isr_subscription_chf if currency == "CHF" else rec.l10n_ch_isr_subscription_eur
|
||||
if subscrip and not _is_l10n_ch_isr_issuer(subscrip, currency):
|
||||
example = "01-162-8" if currency == "CHF" else "03-162-5"
|
||||
raise ValidationError(
|
||||
_("The ISR subcription {} for {} number is not valid.\n"
|
||||
"It must starts with {} and we a valid postal number format. eg. {}"
|
||||
).format(subscrip, currency, ISR_SUBSCRIPTION_CODE[currency], example))
|
||||
return True
|
||||
# fields to configure payment slip generation
|
||||
l10n_ch_display_qr_bank_options = fields.Boolean(compute='_compute_l10n_ch_display_qr_bank_options')
|
||||
|
||||
@api.depends('partner_id', 'company_id')
|
||||
def _compute_l10n_ch_show_subscription(self):
|
||||
def _compute_l10n_ch_display_qr_bank_options(self):
|
||||
for bank in self:
|
||||
if bank.partner_id:
|
||||
bank.l10n_ch_show_subscription = bank.partner_id.ref_company_ids.country_id.code in ('CH', 'LI')
|
||||
bank.l10n_ch_display_qr_bank_options = bank.partner_id.ref_company_ids.country_id.code in ('CH', 'LI')
|
||||
elif bank.company_id:
|
||||
bank.l10n_ch_show_subscription = bank.company_id.account_fiscal_country_id.code in ('CH', 'LI')
|
||||
bank.l10n_ch_display_qr_bank_options = bank.company_id.account_fiscal_country_id.code in ('CH', 'LI')
|
||||
else:
|
||||
bank.l10n_ch_show_subscription = self.env.company.account_fiscal_country_id.code in ('CH', 'LI')
|
||||
|
||||
@api.depends('acc_number', 'acc_type')
|
||||
def _compute_sanitized_acc_number(self):
|
||||
#Only remove spaces in case it is not postal
|
||||
postal_banks = self.filtered(lambda b: b.acc_type == "postal")
|
||||
for bank in postal_banks:
|
||||
bank.sanitized_acc_number = bank.acc_number
|
||||
super(ResPartnerBank, self - postal_banks)._compute_sanitized_acc_number()
|
||||
bank.l10n_ch_display_qr_bank_options = self.env.company.account_fiscal_country_id.code in ('CH', 'LI')
|
||||
|
||||
@api.depends('acc_number')
|
||||
def _compute_l10n_ch_qr_iban(self):
|
||||
|
|
@ -144,7 +69,6 @@ class ResPartnerBank(models.Model):
|
|||
valid_qr_iban = True
|
||||
except ValidationError:
|
||||
valid_qr_iban = False
|
||||
|
||||
if valid_qr_iban:
|
||||
record.l10n_ch_qr_iban = record.sanitized_acc_number
|
||||
else:
|
||||
|
|
@ -164,89 +88,16 @@ class ResPartnerBank(models.Model):
|
|||
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()
|
||||
rslt.append(('postal', _('Postal')))
|
||||
return rslt
|
||||
|
||||
@api.model
|
||||
def retrieve_acc_type(self, acc_number):
|
||||
""" Overridden method enabling the recognition of swiss postal bank
|
||||
account numbers.
|
||||
"""
|
||||
acc_number_split = ""
|
||||
# acc_number_split is needed to continue to recognize the account
|
||||
# as a postal account even if the difference
|
||||
if acc_number and " " in acc_number:
|
||||
acc_number_split = acc_number.split(" ")[0]
|
||||
if _is_l10n_ch_postal(acc_number) or (acc_number_split and _is_l10n_ch_postal(acc_number_split)):
|
||||
return 'postal'
|
||||
else:
|
||||
return super(ResPartnerBank, self).retrieve_acc_type(acc_number)
|
||||
|
||||
@api.depends('acc_number', 'partner_id', 'acc_type')
|
||||
def _compute_l10n_ch_postal(self):
|
||||
for record in self:
|
||||
if record.acc_type == 'iban':
|
||||
record.l10n_ch_postal = self._retrieve_l10n_ch_postal(record.sanitized_acc_number)
|
||||
elif record.acc_type == 'postal':
|
||||
if record.acc_number and " " in record.acc_number:
|
||||
record.l10n_ch_postal = record.acc_number.split(" ")[0]
|
||||
else:
|
||||
record.l10n_ch_postal = record.acc_number
|
||||
# In case of ISR issuer, this number is not
|
||||
# unique and we fill acc_number with partner
|
||||
# name to give proper information to the user
|
||||
if record.partner_id and record.acc_number[:2] in ["01", "03"]:
|
||||
record.acc_number = ("{} {}").format(record.acc_number, record.partner_id.name)
|
||||
|
||||
@api.model
|
||||
def _is_postfinance_iban(self, iban):
|
||||
"""Postfinance IBAN have format
|
||||
CHXX 0900 0XXX XXXX XXXX K
|
||||
Where 09000 is the clearing number
|
||||
"""
|
||||
return iban.startswith(('CH', 'LI')) and iban[4:9] == CLEARING
|
||||
|
||||
@api.model
|
||||
def _pretty_postal_num(self, number):
|
||||
"""format a postal account number or an ISR subscription number
|
||||
as per specifications with '-' separators.
|
||||
eg. 010001628 -> 01-162-8
|
||||
"""
|
||||
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', number or ''):
|
||||
return number
|
||||
currency_code = number[:2]
|
||||
middle_part = number[2:-1]
|
||||
trailing_cipher = number[-1]
|
||||
middle_part = middle_part.lstrip("0")
|
||||
return currency_code + '-' + middle_part + '-' + trailing_cipher
|
||||
|
||||
@api.model
|
||||
def _retrieve_l10n_ch_postal(self, iban):
|
||||
"""Reads a swiss postal account number from a an IBAN and returns it as
|
||||
a string. Returns None if no valid postal account number was found, or
|
||||
the given iban was not from Swiss Postfinance.
|
||||
|
||||
CH09 0900 0000 1000 8060 7 -> 10-8060-7
|
||||
"""
|
||||
if self._is_postfinance_iban(iban):
|
||||
# the IBAN corresponds to a swiss account
|
||||
return self._pretty_postal_num(iban[-9:])
|
||||
return None
|
||||
|
||||
def _l10n_ch_get_qr_vals(self, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
comment = ""
|
||||
if free_communication:
|
||||
comment = (free_communication[:137] + '...') if len(free_communication) > 140 else free_communication
|
||||
|
||||
creditor_addr_1, creditor_addr_2 = self._get_partner_address_lines(self.partner_id)
|
||||
debtor_addr_1, debtor_addr_2 = self._get_partner_address_lines(debtor_partner)
|
||||
cred_street, cred_street_number, cred_zip, cred_city = self._get_partner_address_lines(self.partner_id)
|
||||
debt_street, debt_street_number, debt_zip, debt_city = self._get_partner_address_lines(debtor_partner)
|
||||
|
||||
# Compute reference type (empty by default, only mandatory for QR-IBAN,
|
||||
# and must then be 27 characters-long, with mod10r check digit as the 27th one,
|
||||
# just like ISR number for invoices)
|
||||
# and must then be 27 characters-long, with mod10r check digit as the 27th one)
|
||||
reference_type = 'NON'
|
||||
reference = ''
|
||||
acc_number = self.sanitized_acc_number
|
||||
|
|
@ -262,17 +113,17 @@ class ResPartnerBank(models.Model):
|
|||
|
||||
currency = currency or self.currency_id or self.company_id.currency_id
|
||||
|
||||
return [
|
||||
result = [
|
||||
'SPC', # QR Type
|
||||
'0200', # Version
|
||||
'1', # Coding Type
|
||||
acc_number, # IBAN / QR-IBAN
|
||||
'K', # Creditor Address Type
|
||||
'S', # Creditor Address Type
|
||||
(self.acc_holder_name or self.partner_id.name)[:70], # Creditor Name
|
||||
creditor_addr_1, # Creditor Address Line 1
|
||||
creditor_addr_2, # Creditor Address Line 2
|
||||
'', # Creditor Postal Code (empty, since we're using combined addres elements)
|
||||
'', # Creditor Town (empty, since we're using combined addres elements)
|
||||
cred_street, # Creditor Street Name
|
||||
cred_street_number, # Creditor Building Number
|
||||
cred_zip, # Creditor Postal Code
|
||||
cred_city, # Creditor Town
|
||||
self.partner_id.country_id.code, # Creditor Country
|
||||
'', # Ultimate Creditor Address Type
|
||||
'', # Name
|
||||
|
|
@ -283,19 +134,22 @@ class ResPartnerBank(models.Model):
|
|||
'', # Ultimate Creditor Country
|
||||
'{:.2f}'.format(amount), # Amount
|
||||
currency.name, # Currency
|
||||
'K', # Ultimate Debtor Address Type
|
||||
'S', # Ultimate Debtor Address Type
|
||||
debtor_partner.commercial_partner_id.name[:70], # Ultimate Debtor Name
|
||||
debtor_addr_1, # Ultimate Debtor Address Line 1
|
||||
debtor_addr_2, # Ultimate Debtor Address Line 2
|
||||
'', # Ultimate Debtor Postal Code (not to be provided for address type K)
|
||||
'', # Ultimate Debtor Postal City (not to be provided for address type K)
|
||||
debtor_partner.country_id.code, # Ultimate Debtor Postal Country
|
||||
debt_street, # Ultimate Debtor Street Name
|
||||
debt_street_number, # Ultimate Debtor Building Number
|
||||
debt_zip, # Ultimate Debtor Postal Code
|
||||
debt_city, # Ultimate Debtor Town
|
||||
debtor_partner.country_id.code, # Ultimate Debtor Country
|
||||
reference_type, # Reference Type
|
||||
reference, # Reference
|
||||
comment, # Unstructured Message
|
||||
'EPD', # Mandatory trailer part
|
||||
]
|
||||
|
||||
# newlines shift field content to a different line, causing the QR code to be rejected
|
||||
return [line.replace('\n', ' ') for line in result]
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
|
@ -307,7 +161,7 @@ class ResPartnerBank(models.Model):
|
|||
'barcode_type': 'QR',
|
||||
'width': 256,
|
||||
'height': 256,
|
||||
'quiet': 1,
|
||||
'quiet': 0,
|
||||
'mask': 'ch_cross',
|
||||
'value': '\n'.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
|
||||
# Swiss QR code requires Error Correction Level = 'M' by specification
|
||||
|
|
@ -316,14 +170,28 @@ class ResPartnerBank(models.Model):
|
|||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_partner_address_lines(self, partner):
|
||||
""" Returns a tuple of two elements containing the address lines to use
|
||||
for this partner. Line 1 contains the street and number, line 2 contains
|
||||
zip and city. Those two lines are limited to 70 characters
|
||||
""" Retrieves the partner's address fields, truncated to respect the line specs.
|
||||
:returns: tuple(street, street_number, zip, city)
|
||||
"""
|
||||
streets = [partner.street, partner.street2]
|
||||
line_1 = ' '.join(filter(None, streets))
|
||||
line_2 = partner.zip + ' ' + partner.city
|
||||
return line_1[:70], line_2[:70]
|
||||
street_1_split = street_split(partner.street or '')
|
||||
street_name = street_1_split['street_name']
|
||||
building_number = f"{street_1_split['street_number']} {street_1_split['street_number2']}".strip()
|
||||
|
||||
if building_number:
|
||||
concatenated_building_number = f"{building_number} {partner.street2 or ''}".strip()
|
||||
if len(concatenated_building_number) <= 16:
|
||||
building_number = concatenated_building_number
|
||||
else:
|
||||
# Try to complete the address with street2
|
||||
street_2_split = street_split(partner.street2 or '')
|
||||
|
||||
building_number = f"{street_2_split['street_number']} {street_2_split['street_number2']}".strip()
|
||||
if building_number:
|
||||
street_name = f"{street_name} {street_2_split['street_name']}".strip()
|
||||
else:
|
||||
building_number = (partner.street2 or '').strip()
|
||||
|
||||
return street_name[:70], building_number[:16], partner.zip[:16], partner.city[:35]
|
||||
|
||||
@api.model
|
||||
def _is_qr_reference(self, reference):
|
||||
|
|
@ -331,9 +199,9 @@ class ResPartnerBank(models.Model):
|
|||
made of 27 digits, the 27th being a mod10r check on the 26 previous ones.
|
||||
"""
|
||||
return reference \
|
||||
and len(reference) == 27 \
|
||||
and re.match(r'\d+$', reference) \
|
||||
and reference == mod10r(reference[:-1])
|
||||
and len(reference) == 27 \
|
||||
and re.match(r'\d+$', reference) \
|
||||
and reference == mod10r(reference[:-1])
|
||||
|
||||
@api.model
|
||||
def _is_iso11649_reference(self, reference):
|
||||
|
|
@ -346,21 +214,30 @@ class ResPartnerBank(models.Model):
|
|||
and int(''.join(str(int(x, 36)) for x in clean(reference[4:] + reference[:4], ' -.,/:').upper().strip())) % 97 == 1
|
||||
# see https://github.com/arthurdejong/python-stdnum/blob/master/stdnum/iso11649.py
|
||||
|
||||
def _eligible_for_qr_code(self, qr_method, debtor_partner, currency, raises_error=True):
|
||||
if qr_method == 'ch_qr':
|
||||
error_messages = [_("The QR code could not be generated for the following reason(s):")]
|
||||
def _l10n_ch_qr_debtor_check(self, debtor_partner):
|
||||
""" This method should be used in _get_error_messages_for_qr and _check_for_qr_code_errors
|
||||
It allows is to permit to set this qr method if a partner is not yet provided when executing _get_error_messages_for_qr
|
||||
while preventing to print qr code when executing _check_for_qr_code_errors if the partner is not provided
|
||||
"""
|
||||
if not debtor_partner or debtor_partner.country_id.code not in ('CH', 'LI'):
|
||||
return _("The debtor partner's address isn't located in Switzerland.")
|
||||
return False
|
||||
|
||||
def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency):
|
||||
def _get_error_for_ch_qr():
|
||||
error_messages = [_("The Swiss QR code could not be generated for the following reason(s):")]
|
||||
if self.acc_type != 'iban':
|
||||
error_messages.append(_("The account type isn't QR-IBAN or IBAN."))
|
||||
if not debtor_partner or debtor_partner.country_id.code not in ('CH', 'LI'):
|
||||
error_messages.append(_("The debtor partner's address isn't located in Switzerland."))
|
||||
debtor_check = self._l10n_ch_qr_debtor_check(debtor_partner)
|
||||
if debtor_partner and debtor_check:
|
||||
error_messages.append(debtor_check)
|
||||
if currency.id not in (self.env.ref('base.EUR').id, self.env.ref('base.CHF').id):
|
||||
error_messages.append(_("The currency isn't EUR nor CHF. \r\n"))
|
||||
if len(error_messages) != 1:
|
||||
if raises_error:
|
||||
raise UserError(' '.join(error_messages))
|
||||
return False
|
||||
return True
|
||||
return super()._eligible_for_qr_code(qr_method, debtor_partner, currency, raises_error)
|
||||
error_messages.append(_("The currency isn't EUR nor CHF."))
|
||||
return '\r\n'.join(error_messages) if len(error_messages) > 1 else None
|
||||
|
||||
if qr_method == 'ch_qr':
|
||||
return _get_error_for_ch_qr()
|
||||
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):
|
||||
def _partner_fields_set(partner):
|
||||
|
|
@ -379,6 +256,10 @@ class ResPartnerBank(models.Model):
|
|||
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.")
|
||||
|
||||
debtor_check = self._l10n_ch_qr_debtor_check(debtor_partner)
|
||||
if debtor_check:
|
||||
return debtor_check
|
||||
|
||||
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
@api.model
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_print_bank_location = fields.Boolean(string='Print bank location', default=False, help='Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR.')
|
||||
l10n_ch_isr_scan_line_left = fields.Float(string='Scan line horizontal offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
l10n_ch_isr_scan_line_top = fields.Float(string='Scan line vertical offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
|
||||
|
||||
def _compute_l10n_ch_isr(self):
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
for company in self:
|
||||
company.l10n_ch_isr_preprinted_account = bool(get_param('l10n_ch.isr_preprinted_account', default=False))
|
||||
company.l10n_ch_isr_preprinted_bank = bool(get_param('l10n_ch.isr_preprinted_bank', default=False))
|
||||
company.l10n_ch_isr_scan_line_top = float(get_param('l10n_ch.isr_scan_line_top', default=0))
|
||||
company.l10n_ch_isr_scan_line_left = float(get_param('l10n_ch.isr_scan_line_left', default=0))
|
||||
|
||||
def _set_l10n_ch_isr(self):
|
||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||
for company in self:
|
||||
set_param("l10n_ch.isr_preprinted_account", company.l10n_ch_isr_preprinted_account)
|
||||
set_param("l10n_ch.isr_preprinted_bank", company.l10n_ch_isr_preprinted_bank)
|
||||
set_param("l10n_ch.isr_scan_line_top", company.l10n_ch_isr_scan_line_top)
|
||||
set_param("l10n_ch.isr_scan_line_left", company.l10n_ch_isr_scan_line_left)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account',
|
||||
related="company_id.l10n_ch_isr_preprinted_account", readonly=False)
|
||||
l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank',
|
||||
related="company_id.l10n_ch_isr_preprinted_bank", readonly=False)
|
||||
l10n_ch_isr_print_bank_location = fields.Boolean(string="Print bank on ISR",
|
||||
related="company_id.l10n_ch_isr_print_bank_location", readonly=False,
|
||||
required=True)
|
||||
l10n_ch_isr_scan_line_left = fields.Float(string='Horizontal offset',
|
||||
related="company_id.l10n_ch_isr_scan_line_left", readonly=False)
|
||||
l10n_ch_isr_scan_line_top = fields.Float(string='Vertical offset',
|
||||
related="company_id.l10n_ch_isr_scan_line_top", readonly=False)
|
||||
50
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/template_ch.py
Normal file
50
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/template_ch.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo.addons.account.models.chart_template import template
|
||||
|
||||
|
||||
class AccountChartTemplate(models.AbstractModel):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
@template('ch')
|
||||
def _get_ch_template_data(self):
|
||||
return {
|
||||
'code_digits': '4',
|
||||
'property_account_receivable_id': 'ch_coa_1100',
|
||||
'property_account_payable_id': 'ch_coa_2000',
|
||||
}
|
||||
|
||||
@template('ch', 'res.company')
|
||||
def _get_ch_res_company(self):
|
||||
return {
|
||||
self.env.company.id: {
|
||||
'account_fiscal_country_id': 'base.ch',
|
||||
'bank_account_code_prefix': '102',
|
||||
'cash_account_code_prefix': '100',
|
||||
'transfer_account_code_prefix': '1090',
|
||||
'account_default_pos_receivable_account_id': 'ch_coa_1101',
|
||||
'income_currency_exchange_account_id': 'ch_coa_3806',
|
||||
'expense_currency_exchange_account_id': 'ch_coa_4906',
|
||||
'account_journal_early_pay_discount_loss_account_id': 'ch_coa_4901',
|
||||
'account_journal_early_pay_discount_gain_account_id': 'ch_coa_3801',
|
||||
'default_cash_difference_expense_account_id': 'ch_coa_4991',
|
||||
'default_cash_difference_income_account_id': 'ch_coa_4992',
|
||||
'account_sale_tax_id': 'vat_sale_81',
|
||||
'account_purchase_tax_id': 'vat_purchase_81',
|
||||
'external_report_layout_id': 'l10n_din5008.external_layout_din5008',
|
||||
'paperformat_id': 'l10n_din5008.paperformat_euro_din',
|
||||
'expense_account_id': 'ch_coa_4200',
|
||||
'income_account_id': 'ch_coa_3200',
|
||||
'account_stock_journal_id': 'inventory_valuation',
|
||||
'account_stock_valuation_id': 'ch_coa_1210',
|
||||
},
|
||||
}
|
||||
|
||||
@template('ch', 'account.account')
|
||||
def _get_ch_account_account(self):
|
||||
return {
|
||||
'ch_coa_1210': {
|
||||
'account_stock_expense_id': 'ch_coa_4000',
|
||||
'account_stock_variation_id': 'ch_coa_4801',
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue