summaryrefslogtreecommitdiff
path: root/addons/l10n_ch/models/account_invoice.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/l10n_ch/models/account_invoice.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/l10n_ch/models/account_invoice.py')
-rw-r--r--addons/l10n_ch/models/account_invoice.py352
1 files changed, 352 insertions, 0 deletions
diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py
new file mode 100644
index 00000000..6354167c
--- /dev/null
+++ b/addons/l10n_ch/models/account_invoice.py
@@ -0,0 +1,352 @@
+# -*- 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 ValidationError, 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):
+ _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. ")
+
+ @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
+ self.ensure_one()
+ 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):
+ """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
+ """
+ for record in self:
+ has_qriban = record.partner_bank_id and record.partner_bank_id._is_qr_iban() or False
+ isr_subscription = record.l10n_ch_isr_subscription
+ if (has_qriban or isr_subscription) and record.name:
+ id_number = record._get_isrb_id_number()
+ if id_number:
+ id_number = id_number.zfill(l10n_ch_ISR_ID_NUM_LENGTH)
+ invoice_ref = re.sub('[^\d]', '', record.name)
+ # 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))
+ record.l10n_ch_isr_number = mod10r(id_number + internal_ref)
+ else:
+ record.l10n_ch_isr_number = False
+
+ @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):
+ """ 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.country_id.code == "CH":
+ partner_bank = inv.partner_bank_id
+ if partner_bank:
+ needs_isr_ref = partner_bank._is_qr_iban() or partner_bank._is_isr_issuer()
+ else:
+ needs_isr_ref = False
+ 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 isr_print(self):
+ """ Triggered by the 'Print ISR' button.
+ """
+ self.ensure_one()
+ if self.l10n_ch_isr_valid:
+ self.l10n_ch_isr_sent = True
+ return self.env.ref('l10n_ch.l10n_ch_isr_report').report_action(self)
+ else:
+ raise ValidationError(_("""You cannot generate an ISR yet.\n
+ For this, you need to :\n
+ - set a valid postal account number (or an IBAN referencing one) for your company\n
+ - define its bank\n
+ - associate this bank with a postal reference for the currency used in this invoice\n
+ - fill the 'bank account' field of the invoice with the postal to be used to receive the related payment. A default account will be automatically set for all invoices created after you defined a postal account for your company."""))
+
+ def print_ch_qr_bill(self):
+ """ Triggered by the 'Print QR-bill' button.
+ """
+ self.ensure_one()
+
+ if not self.partner_bank_id._eligible_for_qr_code('ch_qr', self.partner_id, self.currency_id):
+ raise UserError(_("Cannot generate the QR-bill. Please check you have configured the address of your company and debtor. If you are using a QR-IBAN, also check the invoice's payment reference is a QR reference."))
+
+ self.l10n_ch_isr_sent = True
+ return self.env.ref('l10n_ch.l10n_ch_qr_report').report_action(self)
+
+ def action_invoice_sent(self):
+ # OVERRIDE
+ rslt = super(AccountMove, self).action_invoice_sent()
+
+ if self.l10n_ch_isr_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=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()
+ 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