diff --git a/addons/account/models/res_bank.py b/addons/account/models/res_bank.py index bb01198badd..df3835cdd54 100644 --- a/addons/account/models/res_bank.py +++ b/addons/account/models/res_bank.py @@ -151,7 +151,7 @@ class ResPartnerBank(models.Model): return [(code, name) for (code, name, sequence) in all_available] - def _eligible_for_qr_code(self, qr_method, debtor_partner, currency): + def _eligible_for_qr_code(self, qr_method, debtor_partner, currency, raises_error=True): """ Tells whether or not the criteria to apply QR-generation method qr_method are met for a payment on this account, in the given currency, by debtor_partner. This does not impeach generation errors, @@ -167,4 +167,4 @@ class ResPartnerBank(models.Model): Returns None if no error was found, or a string describing the first error encountered so that it can be reported to the user. """ - return None \ No newline at end of file + return None diff --git a/addons/account_qr_code_sepa/models/res_bank.py b/addons/account_qr_code_sepa/models/res_bank.py index 0927b429c1d..29ed6473708 100644 --- a/addons/account_qr_code_sepa/models/res_bank.py +++ b/addons/account_qr_code_sepa/models/res_bank.py @@ -38,7 +38,7 @@ class ResPartnerBank(models.Model): } return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication) - def _eligible_for_qr_code(self, qr_method, debtor_partner, currency): + def _eligible_for_qr_code(self, qr_method, debtor_partner, currency, raises_error=True): if qr_method == 'sct_qr': # Some countries share the same IBAN country code diff --git a/addons/l10n_ch/__manifest__.py b/addons/l10n_ch/__manifest__.py index 0d9b256e0f9..4de7e40085e 100644 --- a/addons/l10n_ch/__manifest__.py +++ b/addons/l10n_ch/__manifest__.py @@ -36,6 +36,7 @@ Here is how it works: 'depends': ['account', 'l10n_multilang', 'base_iban', 'l10n_din5008'], 'data': [ + 'security/ir.model.access.csv', 'data/l10n_ch_chart_data.xml', 'data/account.account.template.csv', 'data/l10n_ch_chart_post_data.xml', @@ -50,6 +51,7 @@ Here is how it works: 'views/account_invoice_view.xml', 'views/res_config_settings_views.xml', 'views/setup_wizard_views.xml', + 'views/qr_invoice_wizard_view.xml' ], 'demo': [ diff --git a/addons/l10n_ch/models/__init__.py b/addons/l10n_ch/models/__init__.py index 9ee1b1e289a..b39307615c9 100644 --- a/addons/l10n_ch/models/__init__.py +++ b/addons/l10n_ch/models/__init__.py @@ -4,7 +4,6 @@ from . import res_config_settings from . import account_invoice from . import account_journal -from . import mail_template from . import res_bank from . import res_company from . import account_bank_statement diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py index 06dc59b8a62..1cc742ac7ae 100644 --- a/addons/l10n_ch/models/account_invoice.py +++ b/addons/l10n_ch/models/account_invoice.py @@ -1,18 +1,24 @@ # -*- 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.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.') @@ -29,6 +35,14 @@ class AccountMove(models.Model): 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: + 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) + @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""" @@ -278,42 +292,11 @@ class AccountMove(models.Model): """ 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: - raise UserError(_("QR-Bill can not be generated on paid invoices. If the invoice is not fully paid, please make sure Recipient Bank field is not empty and try again.")) - - 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: + 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) @@ -346,5 +329,33 @@ class AccountMove(models.Model): while i > 0: spaced_qrr_ref = qrr_ref[max(i-5, 0) : i] + ' ' + spaced_qrr_ref i -= 5 - return spaced_qrr_ref + + 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, + } diff --git a/addons/l10n_ch/models/ir_actions_report.py b/addons/l10n_ch/models/ir_actions_report.py index 78439c16f89..ac27a31a4a3 100644 --- a/addons/l10n_ch/models/ir_actions_report.py +++ b/addons/l10n_ch/models/ir_actions_report.py @@ -1,10 +1,10 @@ # -*- 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 Image as ReportLabImage +from reportlab.graphics.shapes import 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 @@ -26,3 +26,41 @@ class IrActionsReport(models.Model): cross_path = Path(__file__).absolute().parent / CH_QR_CROSS_FILE qr_cross = ReportLabImage((width/2 - cross_width/2) / mm, (height/2 - cross_height/2) / mm, cross_width / mm, cross_height / mm, cross_path.as_posix()) barcode_drawing.add(qr_cross) + + def _render_qweb_pdf_prepare_streams(self, data, res_ids=None): + # OVERRIDE + res = super()._render_qweb_pdf_prepare_streams(data, res_ids) + if res_ids and self.report_name in ('account.report_invoice_with_payments', 'account.report_invoice'): + invoices = self.env[self.model].browse(res_ids) + # Determine which invoices need a QR/ISR. + qr_inv_ids = [] + isr_inv_ids = [] + for invoice in invoices: + if invoice.company_id.country_code != 'CH': + continue + if invoice.l10n_ch_is_qr_valid: + qr_inv_ids.append(invoice.id) + elif 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.env.ref('l10n_ch.l10n_ch_qr_report')._render_qweb_pdf_prepare_streams(data, res_ids=qr_inv_ids) + for invoice_id, stream in qr_res.items(): + streams_to_append[invoice_id] = stream + if isr_inv_ids: + isr_res = self.env.ref('l10n_ch.l10n_ch_isr_report')._render_qweb_pdf_prepare_streams(data, res_ids=qr_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 diff --git a/addons/l10n_ch/models/mail_template.py b/addons/l10n_ch/models/mail_template.py deleted file mode 100644 index 7d50d1eb5e2..00000000000 --- a/addons/l10n_ch/models/mail_template.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import base64 - -from odoo import api, models - - -class MailTemplate(models.Model): - _inherit = 'mail.template' - - def generate_email(self, res_ids, fields): - """ Method overridden in order to add an attachment containing the ISR - to the draft message when opening the 'send by mail' wizard on an invoice. - This attachment generation will only occur if all the required data are - present on the invoice. Otherwise, no ISR attachment will be created, and - the mail will only contain the invoice (as defined in the mother method). - """ - result = super(MailTemplate, self).generate_email(res_ids, fields) - if self.model != 'account.move': - return result - - multi_mode = True - if isinstance(res_ids, int): - res_ids = [res_ids] - multi_mode = False - - if self.model == 'account.move': - for record in self.env[self.model].browse(res_ids): - inv_print_name = self._render_field('report_name', record.ids, compute_lang=True)[record.id] - new_attachments = [] - - if record.l10n_ch_isr_valid: - # We add an attachment containing the ISR - isr_report_name = 'ISR-' + inv_print_name + '.pdf' - isr_pdf = self.env.ref('l10n_ch.l10n_ch_isr_report')._render_qweb_pdf(record.ids)[0] - isr_pdf = base64.b64encode(isr_pdf) - new_attachments.append((isr_report_name, isr_pdf)) - - if record.partner_bank_id._eligible_for_qr_code('ch_qr', record.partner_id, record.currency_id): - # We add an attachment containing the QR-bill - qr_report_name = 'QR-bill-' + inv_print_name + '.pdf' - qr_pdf = self.env.ref('l10n_ch.l10n_ch_qr_report')._render_qweb_pdf(record.ids)[0] - qr_pdf = base64.b64encode(qr_pdf) - new_attachments.append((qr_report_name, qr_pdf)) - - record_dict = multi_mode and result[record.id] or result - attachments_list = record_dict.get('attachments', False) - if attachments_list: - attachments_list.extend(new_attachments) - else: - record_dict['attachments'] = new_attachments - - return result diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py index 81ec17034ef..1d1fffce6a2 100644 --- a/addons/l10n_ch/models/res_bank.py +++ b/addons/l10n_ch/models/res_bank.py @@ -6,10 +6,9 @@ import re 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 +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]$') @@ -332,15 +331,23 @@ class ResPartnerBank(models.Model): and re.match('\d+$', reference) \ and reference == mod10r(reference[:-1]) - def _eligible_for_qr_code(self, qr_method, debtor_partner, currency): + def _eligible_for_qr_code(self, qr_method, debtor_partner, currency, raises_error=True): if qr_method == 'ch_qr': - - return self.acc_type == 'iban' and \ - self.partner_id.country_id.code == 'CH' and \ - (not debtor_partner or debtor_partner.country_id.code == 'CH') \ - and currency.name in ('EUR', 'CHF') - - return super()._eligible_for_qr_code(qr_method, debtor_partner, currency) + 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.")) + if self.partner_id.country_id.code != 'CH': + error_messages.append(_("Your company isn't located in Switzerland.")) + if not debtor_partner or debtor_partner.country_id.code != 'CH': + 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): diff --git a/addons/l10n_ch/report/swissqr_report.py b/addons/l10n_ch/report/swissqr_report.py index 178777cb035..c10e3959a12 100644 --- a/addons/l10n_ch/report/swissqr_report.py +++ b/addons/l10n_ch/report/swissqr_report.py @@ -1,5 +1,4 @@ # -*- coding:utf-8 -*- - from odoo import api, models class ReportSwissQR(models.AbstractModel): @@ -19,4 +18,4 @@ class ReportSwissQR(models.AbstractModel): 'doc_model': 'account.move', 'docs': docs, 'qr_code_urls': qr_code_urls, - } \ No newline at end of file + } diff --git a/addons/l10n_ch/report/swissqr_report.xml b/addons/l10n_ch/report/swissqr_report.xml index ed18b919b5a..1a813117efe 100644 --- a/addons/l10n_ch/report/swissqr_report.xml +++ b/addons/l10n_ch/report/swissqr_report.xml @@ -10,7 +10,6 @@ l10n_ch.qr_report_main 'QR-bill-%s' % object.name - 'QR-bill-' + object.name + '.pdf'