mirror of
https://gitlab.com/flectra-community/l10n-switzerland-flectra.git
synced 2024-11-17 03:22:03 +00:00
344 lines
12 KiB
Python
344 lines
12 KiB
Python
|
# Copyright 2019-TODAY Flectra
|
||
|
# Copyright 2019-TODAY Camptocamp SA
|
||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||
|
|
||
|
import re
|
||
|
|
||
|
import werkzeug.urls
|
||
|
|
||
|
from flectra import _, api, fields, models
|
||
|
from flectra.exceptions import ValidationError, UserError
|
||
|
from flectra.tools.misc import mod10r
|
||
|
from flectra.addons.base.models.res_bank import sanitize_account_number
|
||
|
|
||
|
|
||
|
MSG_INCOMPLETE_PARTNER_ADDR = _(
|
||
|
"- Partner address is incomplete, it must contain name, street, zip, city"
|
||
|
" and country. Country must be Switzerland"
|
||
|
)
|
||
|
|
||
|
MSG_INCOMPLETE_COMPANY_ADDR = _(
|
||
|
"- Company address is incomplete, it must contain name, street, zip, city"
|
||
|
" and country. Country must be Switzerland"
|
||
|
)
|
||
|
|
||
|
MSG_NO_BANK_ACCOUNT = _(
|
||
|
"- Invoice's 'Bank Account' is empty. You need to create or select a valid"
|
||
|
" IBAN or QR-IBAN account"
|
||
|
)
|
||
|
|
||
|
MSG_BAD_QRR = _(
|
||
|
"- With a QR-IBAN a valid QRR must be used."
|
||
|
)
|
||
|
|
||
|
ERROR_MESSAGES = {
|
||
|
"incomplete_partner_address": MSG_INCOMPLETE_PARTNER_ADDR,
|
||
|
"incomplete_company_address": MSG_INCOMPLETE_COMPANY_ADDR,
|
||
|
"no_bank_account": MSG_NO_BANK_ACCOUNT,
|
||
|
"bad_qrr": MSG_BAD_QRR,
|
||
|
}
|
||
|
|
||
|
|
||
|
class AccountInvoice(models.Model):
|
||
|
_inherit = 'account.move'
|
||
|
|
||
|
l10n_ch_qrr_name = fields.Char(
|
||
|
"QR Reference Number",
|
||
|
copy=False,
|
||
|
help="Automatic generated QR Reference Number")
|
||
|
|
||
|
l10n_ch_qrr = fields.Char(
|
||
|
compute='_compute_l10n_ch_qrr',
|
||
|
store=True,
|
||
|
help='The reference QRR associated with this invoice',
|
||
|
)
|
||
|
|
||
|
l10n_ch_qrr_spaced = fields.Char(
|
||
|
compute='_compute_l10n_ch_qrr_spaced',
|
||
|
help=(
|
||
|
"Reference QRR split in blocks of 5 characters (right-justified),"
|
||
|
"to generate QR-bill report."
|
||
|
),
|
||
|
)
|
||
|
# This field is used in the "invisible" condition field of the 'Print QRR' button.
|
||
|
l10n_ch_currency_name = fields.Char(
|
||
|
related='currency_id.name',
|
||
|
readonly=True,
|
||
|
string="Currency Name",
|
||
|
help="The name of this invoice's currency",
|
||
|
)
|
||
|
l10n_ch_qrr_sent = fields.Boolean(
|
||
|
default=False,
|
||
|
help=(
|
||
|
"Boolean value telling whether or not the QRR corresponding to"
|
||
|
" this invoice has already been printed or sent by mail.",
|
||
|
),
|
||
|
)
|
||
|
|
||
|
def _get_qrr_prefix(self):
|
||
|
"""Hook to add a customized prefix"""
|
||
|
self.ensure_one()
|
||
|
return ''
|
||
|
|
||
|
#@api.depends('number', 'name')
|
||
|
@api.depends('name') # To be add number in depends or not sure
|
||
|
def _compute_l10n_ch_qrr(self):
|
||
|
r""" Compute a QRR reference
|
||
|
|
||
|
QRR is the replacement of ISR
|
||
|
The QRR reference number is 27 characters long.
|
||
|
|
||
|
To generate the QRR reference, the we use the invoice sequence number,
|
||
|
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).
|
||
|
|
||
|
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
|
||
|
|
||
|
"""
|
||
|
for record in self:
|
||
|
if record.has_qrr():
|
||
|
record.l10n_ch_qrr = record.l10n_ch_qrr_name
|
||
|
elif record.name:
|
||
|
prefix = record._get_qrr_prefix()
|
||
|
invoice_ref = re.sub(r'[^\d]', '', record.name)
|
||
|
# keep only the last digits if it exceed boundaries
|
||
|
full_len = len(prefix) + len(invoice_ref)
|
||
|
extra = full_len - 26
|
||
|
if extra > 0:
|
||
|
invoice_ref = invoice_ref[extra:]
|
||
|
internal_ref = invoice_ref.zfill(26 - len(prefix))
|
||
|
record.l10n_ch_qrr = mod10r(prefix + internal_ref)
|
||
|
else:
|
||
|
record.l10n_ch_qrr = False
|
||
|
|
||
|
@api.depends('l10n_ch_qrr')
|
||
|
def _compute_l10n_ch_qrr_spaced(self):
|
||
|
def _space_qrr(ref):
|
||
|
to_treat = ref
|
||
|
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:
|
||
|
spaced_qrr = False
|
||
|
if record.l10n_ch_qrr:
|
||
|
spaced_qrr = _space_qrr(record.l10n_ch_qrr)
|
||
|
record.l10n_ch_qrr_spaced = spaced_qrr
|
||
|
|
||
|
def _get_communications(self):
|
||
|
if self.has_qrr():
|
||
|
structured_communication = self.l10n_ch_qrr
|
||
|
free_communication = ''
|
||
|
else:
|
||
|
structured_communication = ''
|
||
|
free_communication = self.name
|
||
|
free_communication = free_communication or self.name # to be replace self.number not sure
|
||
|
|
||
|
additional_info = ""
|
||
|
if free_communication:
|
||
|
additional_info = (
|
||
|
(free_communication[:137] + '...')
|
||
|
if len(free_communication) > 140
|
||
|
else free_communication
|
||
|
)
|
||
|
|
||
|
# 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 = ''
|
||
|
if self.has_qrr():
|
||
|
# _check_for_qr_code_errors ensures we can't have a QR-IBAN
|
||
|
# without a QR-reference here
|
||
|
reference_type = 'QRR'
|
||
|
reference = structured_communication
|
||
|
return reference_type, reference, additional_info
|
||
|
|
||
|
def _prepare_swiss_code_url_vals(self):
|
||
|
|
||
|
reference_type, reference, additional_info = self._get_communications()
|
||
|
|
||
|
creditor = self.company_id.partner_id
|
||
|
debtor = self.commercial_partner_id
|
||
|
|
||
|
creditor_addr_1, creditor_addr_2 = self._get_partner_address_lines(
|
||
|
creditor
|
||
|
)
|
||
|
debtor_addr_1, debtor_addr_2 = self._get_partner_address_lines(debtor)
|
||
|
|
||
|
amount = '{:.2f}'.format(self.amount_residual)
|
||
|
acc_number = self.partner_bank_id.sanitized_acc_number
|
||
|
|
||
|
# If there is a QR IBAN we use it for the barcode instead of the
|
||
|
# account number
|
||
|
qr_iban = self.partner_bank_id.l10n_ch_qr_iban
|
||
|
if qr_iban:
|
||
|
acc_number = sanitize_account_number(qr_iban)
|
||
|
|
||
|
# fmt: off
|
||
|
qr_code_vals = [
|
||
|
'SPC', # QR Type
|
||
|
'0200', # Version
|
||
|
'1', # Coding Type
|
||
|
acc_number, # IBAN
|
||
|
# Creditor
|
||
|
'K', # - Address Type
|
||
|
creditor.name[:70], # - Name
|
||
|
creditor_addr_1, # - Address Line 1
|
||
|
creditor_addr_2, # - Address Line 2
|
||
|
'', # - Postal Code (not used in type K)
|
||
|
'', # - City (not used in type K)
|
||
|
creditor.country_id.code, # - Country
|
||
|
# Ultimate Creditor
|
||
|
'', # - Address Type
|
||
|
'', # - Name
|
||
|
'', # - Address Line 1
|
||
|
'', # - Address Line 2
|
||
|
'', # - Postal Code
|
||
|
'', # - Town
|
||
|
'', # - Country
|
||
|
amount, # Amount
|
||
|
self.currency_id.name, # Currency
|
||
|
# Ultimate Debtor
|
||
|
'K', # - Address Type
|
||
|
debtor.name[:70], # - Name
|
||
|
debtor_addr_1, # - Address Line 1
|
||
|
debtor_addr_2, # - Address Line 2
|
||
|
'', # - Postal Code (not used in type K)
|
||
|
'', # - City (not used in type K)
|
||
|
debtor.country_id.code, # - Country
|
||
|
reference_type, # Reference Type
|
||
|
reference, # Reference
|
||
|
additional_info, # Unstructured Message
|
||
|
'EPD', # Mandatory trailer part
|
||
|
'', # Bill information
|
||
|
]
|
||
|
# fmt: on
|
||
|
return qr_code_vals
|
||
|
|
||
|
#@api.multi
|
||
|
def build_swiss_code_url(self):
|
||
|
|
||
|
self.ensure_one()
|
||
|
|
||
|
qr_code_vals = self._prepare_swiss_code_url_vals()
|
||
|
|
||
|
# use quiet to remove blank around the QR and make it easier to place it
|
||
|
return '/report/qrcode/?value=%s&width=%s&height=%s&bar_border=0' % (
|
||
|
werkzeug.urls.url_quote_plus('\n'.join(qr_code_vals)),
|
||
|
256,
|
||
|
256,
|
||
|
)
|
||
|
|
||
|
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_qrr(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])
|
||
|
)
|
||
|
|
||
|
def validate_swiss_code_arguments(self):
|
||
|
# TODO do checks separately
|
||
|
reference_to_check = self.l10n_ch_qrr_name
|
||
|
|
||
|
def _partner_fields_set(partner):
|
||
|
return (
|
||
|
partner.zip
|
||
|
and partner.city
|
||
|
and partner.country_id.code
|
||
|
and (partner.street or partner.street2)
|
||
|
)
|
||
|
|
||
|
errors = []
|
||
|
if not _partner_fields_set(self.partner_id):
|
||
|
errors.append(_("incomplete_partner_address"))
|
||
|
if not self.partner_bank_id:
|
||
|
errors.append(_("no_bank_account"))
|
||
|
elif not _partner_fields_set(self.partner_bank_id.partner_id):
|
||
|
errors.append(_("incomplete_company_address"))
|
||
|
if (reference_to_check
|
||
|
and self.partner_bank_id._is_qr_iban()
|
||
|
and not self._is_qrr(reference_to_check)):
|
||
|
errors.append(_("bad_qrr"))
|
||
|
|
||
|
if len(errors) > 0:
|
||
|
raise UserError(errors)
|
||
|
|
||
|
return not errors
|
||
|
|
||
|
def can_generate_qr_bill(self, returned_errors=None):
|
||
|
""" Returns True if the invoice can be used to generate a QR-bill.
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
return self.validate_swiss_code_arguments()
|
||
|
|
||
|
def print_ch_qr_bill(self):
|
||
|
""" Triggered by the 'Print QR-bill' button.
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
|
||
|
if not self.can_generate_qr_bill():
|
||
|
msg_error_list = "\n".join(ERROR_MESSAGES[e] for e in self.errors)
|
||
|
raise ValidationError(
|
||
|
_("You cannot generate the QR-bill.\n"
|
||
|
"Here is what is blocking:\n"
|
||
|
"{}").format(msg_error_list)
|
||
|
)
|
||
|
|
||
|
self.l10n_ch_qrr_sent = True
|
||
|
return self.env.ref('l10n_ch_qr_bill.l10n_ch_qr_report').report_action(self)
|
||
|
|
||
|
def has_qrr(self):
|
||
|
"""Check if this invoice has a valid QRR reference (for Switzerland)
|
||
|
|
||
|
"""
|
||
|
return self._is_qrr(self.l10n_ch_qrr_name)
|
||
|
|
||
|
def _validate_qrr(self):
|
||
|
partner_bank = self.partner_bank_id
|
||
|
if partner_bank._is_qr_iban() and not self.has_qrr():
|
||
|
raise ValidationError(
|
||
|
_("""The payment reference is not a valid QR Reference.""")
|
||
|
)
|
||
|
return True
|
||
|
|
||
|
def action_invoice_open(self):
|
||
|
res = super(AccountInvoice, self).action_invoice_open()
|
||
|
for rec in self:
|
||
|
# Define reference once number has been generated
|
||
|
if rec.type == "out_invoice":
|
||
|
if not rec.l10n_ch_qrr_name and rec.partner_bank_id._is_qr_iban():
|
||
|
rec.l10n_ch_qrr_name = rec.l10n_ch_qrr
|
||
|
rec._validate_qrr()
|
||
|
return res
|