l10n-switzerland-flectra/l10n_ch_qr_bill/models/account_invoice.py

344 lines
12 KiB
Python
Raw Normal View History

# 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