mirror of
https://github.com/bringout/oca-ocb-l10n_europe.git
synced 2026-04-27 10:02:02 +02:00
Initial commit: L10N_Europe packages
This commit is contained in:
commit
9803722600
2377 changed files with 380711 additions and 0 deletions
11
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/__init__.py
Normal file
11
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_config_settings
|
||||
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
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# -*- 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()
|
||||
391
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/account_invoice.py
Normal file
391
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/account_invoice.py
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@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.
|
||||
|
||||
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)
|
||||
# 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))
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
`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
|
||||
|
||||
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
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number
|
||||
|
||||
@api.model
|
||||
def space_qrr_reference(self, qrr_ref):
|
||||
""" Makes the provided QRR reference human-friendly, spacing its elements
|
||||
by blocks of 5 from right to left.
|
||||
"""
|
||||
spaced_qrr_ref = ''
|
||||
i = len(qrr_ref) # i is the index after the last index to consider in substrings
|
||||
while i > 0:
|
||||
spaced_qrr_ref = qrr_ref[max(i-5, 0) : i] + ' ' + spaced_qrr_ref
|
||||
i -= 5
|
||||
return spaced_qrr_ref
|
||||
|
||||
@api.model
|
||||
def space_scor_reference(self, iso11649_ref):
|
||||
""" 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):
|
||||
'''
|
||||
Checks that all invoices can be printed in the QR format.
|
||||
If so, launches the printing action.
|
||||
Else, triggers the l10n_ch wizard that will display the informations.
|
||||
'''
|
||||
if any(x.move_type != 'out_invoice' for x in self):
|
||||
raise UserError(_("Only customers invoices can be QR-printed."))
|
||||
if False in self.mapped('l10n_ch_is_qr_valid'):
|
||||
return {
|
||||
'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},
|
||||
}
|
||||
return self.env.ref('account.account_invoices').report_action(self)
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.base_iban.models.res_partner_bank import validate_iban
|
||||
from odoo.addons.base.models.res_bank import sanitize_account_number
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
invoice_reference_model = fields.Selection(selection_add=[
|
||||
('ch', 'Switzerland')
|
||||
], 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
|
||||
'''
|
||||
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)
|
||||
return order_reference
|
||||
return super()._process_reference_for_sale_order(order_reference)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- 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
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import io
|
||||
from odoo import api, models
|
||||
from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
|
||||
from pathlib import Path
|
||||
from reportlab.graphics.shapes import Drawing as ReportLabDrawing, Image as ReportLabImage
|
||||
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'
|
||||
|
||||
@api.model
|
||||
def get_available_barcode_masks(self):
|
||||
rslt = super(IrActionsReport, self).get_available_barcode_masks()
|
||||
rslt['ch_cross'] = self.apply_qr_code_ch_cross_mask
|
||||
return rslt
|
||||
|
||||
@api.model
|
||||
def apply_qr_code_ch_cross_mask(self, width, height, barcode_drawing):
|
||||
assert isinstance(barcode_drawing, ReportLabDrawing)
|
||||
zoom_x = barcode_drawing.transform[0]
|
||||
zoom_y = barcode_drawing.transform[3]
|
||||
cross_width = CH_QR_CROSS_SIZE_RATIO * width
|
||||
cross_height = CH_QR_CROSS_SIZE_RATIO * height
|
||||
cross_path = Path(__file__).absolute().parent / CH_QR_CROSS_FILE
|
||||
qr_cross = ReportLabImage((width/2 - cross_width/2) / zoom_x, (height/2 - cross_height/2) / zoom_y, cross_width / zoom_x, cross_height / zoom_y, cross_path.as_posix())
|
||||
barcode_drawing.add(qr_cross)
|
||||
|
||||
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
|
||||
# OVERRIDE
|
||||
res = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids)
|
||||
if not res_ids:
|
||||
return res
|
||||
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 = {}
|
||||
if qr_inv_ids:
|
||||
qr_res = self._render_qweb_pdf_prepare_streams(
|
||||
'l10n_ch.l10n_ch_qr_report',
|
||||
{
|
||||
**data,
|
||||
'skip_headers': False,
|
||||
},
|
||||
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)
|
||||
header_pdf = OdooPdfFileReader(header_res[invoice_id]['stream'], strict=False)
|
||||
|
||||
page = header_pdf.getPage(0)
|
||||
page.mergePage(qr_pdf.getPage(0))
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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):
|
||||
if self.env.context.get('snailmail_layout'):
|
||||
if self.report_name == 'l10n_ch.qr_report_main':
|
||||
return self.env.ref('l10n_ch.paperformat_euro_no_margin')
|
||||
if self.report_name == 'l10n_ch.qr_report_header':
|
||||
return self.env.ref('l10n_din5008.paperformat_euro_din')
|
||||
return super(IrActionsReport, self).get_paperformat()
|
||||
388
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/res_bank.py
Normal file
388
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/res_bank.py
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
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.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]$')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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] not in ['CH', 'LI']:
|
||||
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'
|
||||
|
||||
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,
|
||||
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')
|
||||
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
|
||||
|
||||
@api.depends('partner_id', 'company_id')
|
||||
def _compute_l10n_ch_show_subscription(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')
|
||||
elif bank.company_id:
|
||||
bank.l10n_ch_show_subscription = 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()
|
||||
|
||||
@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_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
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_list)
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
# 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)
|
||||
reference_type = 'NON'
|
||||
reference = ''
|
||||
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)
|
||||
elif self._is_iso11649_reference(structured_communication):
|
||||
reference_type = 'SCOR'
|
||||
reference = structured_communication.replace(' ', '')
|
||||
|
||||
currency = currency or self.currency_id or self.company_id.currency_id
|
||||
|
||||
return [
|
||||
'SPC', # QR Type
|
||||
'0200', # Version
|
||||
'1', # Coding Type
|
||||
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
|
||||
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)
|
||||
self.partner_id.country_id.code, # Creditor Country
|
||||
'', # Ultimate Creditor Address Type
|
||||
'', # Name
|
||||
'', # Ultimate Creditor Address Line 1
|
||||
'', # Ultimate Creditor Address Line 2
|
||||
'', # Ultimate Creditor Postal Code
|
||||
'', # Ultimate Creditor Town
|
||||
'', # Ultimate Creditor Country
|
||||
'{:.2f}'.format(amount), # Amount
|
||||
currency.name, # Currency
|
||||
'K', # 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
|
||||
reference_type, # Reference Type
|
||||
reference, # Reference
|
||||
comment, # Unstructured Message
|
||||
'EPD', # Mandatory trailer part
|
||||
]
|
||||
|
||||
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)
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return {
|
||||
'barcode_type': 'QR',
|
||||
'width': 256,
|
||||
'height': 256,
|
||||
'quiet': 1,
|
||||
'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
|
||||
'barLevel': 'M',
|
||||
}
|
||||
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
|
||||
"""
|
||||
streets = [partner.street, partner.street2]
|
||||
line_1 = ' '.join(filter(None, streets))
|
||||
line_2 = partner.zip + ' ' + partner.city
|
||||
return line_1[:70], line_2[:70]
|
||||
|
||||
@api.model
|
||||
def _is_qr_reference(self, reference):
|
||||
""" Checks whether the given reference is a QR-reference, i.e. it is
|
||||
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])
|
||||
|
||||
@api.model
|
||||
def _is_iso11649_reference(self, reference):
|
||||
""" Checks whether the given reference is a ISO11649 (SCOR) reference.
|
||||
"""
|
||||
return reference \
|
||||
and len(reference) >= 5 \
|
||||
and len(reference) <= 25 \
|
||||
and reference.startswith('RF') \
|
||||
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):")]
|
||||
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."))
|
||||
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)
|
||||
|
||||
def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
def _partner_fields_set(partner):
|
||||
return partner.zip and \
|
||||
partner.city and \
|
||||
partner.country_id.code and \
|
||||
(partner.street or partner.street2)
|
||||
|
||||
if qr_method == 'ch_qr':
|
||||
if not _partner_fields_set(self.partner_id):
|
||||
return _("The partner set on the bank account meant to receive the payment (%s) must have a complete postal address (street, zip, city and country).", self.acc_number)
|
||||
|
||||
if debtor_partner and not _partner_fields_set(debtor_partner):
|
||||
return _("The partner must have a complete postal address (street, zip, city and country).")
|
||||
|
||||
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)
|
||||
|
||||
@api.model
|
||||
def _get_available_qr_methods(self):
|
||||
rslt = super()._get_available_qr_methods()
|
||||
rslt.append(('ch_qr', _("Swiss QR bill"), 10))
|
||||
return rslt
|
||||
29
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/res_company.py
Normal file
29
odoo-bringout-oca-ocb-l10n_ch/l10n_ch/models/res_company.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# -*- 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)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue