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'
diff --git a/addons/l10n_ch/security/ir.model.access.csv b/addons/l10n_ch/security/ir.model.access.csv
new file mode 100644
index 00000000000..2f3dbde4988
--- /dev/null
+++ b/addons/l10n_ch/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_l10n_ch_qr_invoice_wizard,l10n_ch.qr_invoice.wizard,model_l10n_ch_qr_invoice_wizard,account.group_account_invoice,1,1,1,0
diff --git a/addons/l10n_ch/static/src/scss/report_swissqr.scss b/addons/l10n_ch/static/src/scss/report_swissqr.scss
index 23b89381267..9e137a11495 100644
--- a/addons/l10n_ch/static/src/scss/report_swissqr.scss
+++ b/addons/l10n_ch/static/src/scss/report_swissqr.scss
@@ -12,6 +12,10 @@ $l10n-ch-qr-ratio: 1.25;
body.l10n_ch_qr, body[data-oe-report="l10n_ch.l10n_ch_qr_report"] {
padding: 0;
+ span{
+ padding-left: 10mm;
+ white-space: nowrap;
+ }
/* Disable custom bakground */
.o_report_layout_striped {
@@ -26,6 +30,10 @@ body.l10n_ch_qr, body[data-oe-report="l10n_ch.l10n_ch_qr_report"] {
height: 7mm * $l10n-ch-qr-ratio;
padding: 15px;
padding-top: 150px;
+ h1{
+ padding-left:20mm;
+ white-space:nowrap;
+ }
}
.swissqr_content_v2 {
diff --git a/addons/l10n_ch/tests/__init__.py b/addons/l10n_ch/tests/__init__.py
index e9792e787e3..00d72602685 100644
--- a/addons/l10n_ch/tests/__init__.py
+++ b/addons/l10n_ch/tests/__init__.py
@@ -4,6 +4,7 @@
from . import test_ch_qr_code
from . import test_swissqr
from . import test_l10n_ch_isr_print
+from . import test_l10n_ch_qr_print
from . import test_vendor_bill_isr
from . import test_onchange_l10n_ch_postal
from . import test_gen_isr_reference
diff --git a/addons/l10n_ch/tests/test_l10n_ch_isr_print.py b/addons/l10n_ch/tests/test_l10n_ch_isr_print.py
index 1770dd15223..a28501f540a 100644
--- a/addons/l10n_ch/tests/test_l10n_ch_isr_print.py
+++ b/addons/l10n_ch/tests/test_l10n_ch_isr_print.py
@@ -14,7 +14,7 @@ class ISRTest(AccountTestInvoicingCommon):
def print_isr(self, invoice):
try:
- invoice.isr_print()
+ invoice.action_invoice_sent()
return True
except ValidationError:
return False
@@ -69,4 +69,7 @@ class ISRTest(AccountTestInvoicingCommon):
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id})],
})
invoice_eur.action_post()
- self.assertFalse(self.print_isr(invoice_eur))
+ #a normal invoice will still get printed
+ self.assertTrue(self.print_isr(invoice_eur))
+ # However, a isr bill can't be printed with those infos
+ self.assertFalse(invoice_eur.l10n_ch_isr_valid)
diff --git a/addons/l10n_ch/tests/test_l10n_ch_qr_print.py b/addons/l10n_ch/tests/test_l10n_ch_qr_print.py
new file mode 100644
index 00000000000..6da8bb5b7e4
--- /dev/null
+++ b/addons/l10n_ch/tests/test_l10n_ch_qr_print.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+
+_logger = logging.getLogger(__name__)
+
+@tagged('post_install_l10n', 'post_install', '-at_install')
+class QRPrintTest(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref='l10n_ch.l10nch_chart_template'):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+ # the partner must be located in Switzerland.
+ cls.partner = cls.env['res.partner'].create({
+ 'name': 'Bobby',
+ 'country_id': cls.env.ref('base.ch').id,
+ })
+ # The bank account must be QR-compatible
+ cls.qr_bank_account = cls.env['res.partner.bank'].create({
+ 'acc_number': "CH4431999123000889012",
+ 'partner_id': cls.env.company.partner_id.id,
+ 'l10n_ch_isr_subscription_chf': '01-39139-1',
+ })
+ cls.correct_invoice_chf = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner.id,
+ 'partner_bank_id': cls.qr_bank_account.id,
+ 'currency_id': cls.env.ref('base.CHF').id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id})],
+ })
+
+ cls.correct_invoice_eur = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner.id,
+ 'partner_bank_id': cls.qr_bank_account.id,
+ 'currency_id': cls.env.ref('base.EUR').id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id})],
+ })
+
+ cls.wrong_partner_invoice = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner_a.id,
+ 'partner_bank_id': cls.qr_bank_account.id,
+ 'currency_id': cls.env.ref('base.EUR').id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id})],
+ })
+
+ def print_qr_bill(self, invoice):
+ try:
+ invoice.action_invoice_sent()
+ return True
+ except UserError as e:
+ _logger.warning(e.name)
+ return False
+
+ def test_print_qr(self):
+ self.correct_invoice_chf.action_post()
+ self.assertTrue(self.print_qr_bill(self.correct_invoice_chf))
+
+ #The QR can also be printed if the currency is EUR
+ self.env.ref('base.EUR').active = True
+ self.correct_invoice_eur.action_post()
+ self.assertTrue(self.print_qr_bill(self.correct_invoice_eur))
+
+ #A normal invoice will be printed if the partner is not from Switzerland
+ self.wrong_partner_invoice.action_post()
+ self.assertTrue(self.print_qr_bill(self.wrong_partner_invoice))
+
+ #However, a qr bill can't be printed with those infos
+ self.assertFalse(self.wrong_partner_invoice.l10n_ch_is_qr_valid)
diff --git a/addons/l10n_ch/tests/test_swissqr.py b/addons/l10n_ch/tests/test_swissqr.py
index d2fa7aec2b5..b1756ece853 100644
--- a/addons/l10n_ch/tests/test_swissqr.py
+++ b/addons/l10n_ch/tests/test_swissqr.py
@@ -3,6 +3,7 @@
import time
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.exceptions import UserError
from odoo.tests import tagged
CH_IBAN = 'CH15 3881 5158 3845 3843 7'
@@ -164,8 +165,9 @@ class TestSwissQR(AccountTestInvoicingCommon):
def test_swissQR_missing_bank(self):
# Let us test the generation of a SwissQR for an invoice, first by showing an
# QR is included in the invoice is only generated when Odoo has all the data it needs.
- self.invoice1.action_post()
- self.swissqr_not_generated(self.invoice1)
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.invoice1.action_post()
+ self.swissqr_not_generated(self.invoice1)
def test_swissQR_iban(self):
# Now we add an account for payment to our invoice
diff --git a/addons/l10n_ch/views/account_invoice_view.xml b/addons/l10n_ch/views/account_invoice_view.xml
index 3de4c658e60..6b428457764 100644
--- a/addons/l10n_ch/views/account_invoice_view.xml
+++ b/addons/l10n_ch/views/account_invoice_view.xml
@@ -9,58 +9,7 @@
-
-
-
-
-
-
-
-
-
-
+
@@ -90,5 +39,17 @@
account.move
+
+
+ Print QR Invoices
+
+
+ list
+ code
+
+if records:
+ action = records.l10n_ch_action_print_qr()
+
+
diff --git a/addons/l10n_ch/views/qr_invoice_wizard_view.xml b/addons/l10n_ch/views/qr_invoice_wizard_view.xml
new file mode 100644
index 00000000000..9031f9f5db9
--- /dev/null
+++ b/addons/l10n_ch/views/qr_invoice_wizard_view.xml
@@ -0,0 +1,38 @@
+
+
+
+ l10n_ch.qr_invoice.wizard.form
+ l10n_ch.qr_invoice.wizard
+
+
+
+
+
+
+ Qr Batch error Wizard
+ ir.actions.act_window
+ l10n_ch.qr_invoice.wizard
+ form
+
+ new
+
+
diff --git a/addons/l10n_ch/wizard/__init__.py b/addons/l10n_ch/wizard/__init__.py
index 792b395aa76..e617838c86f 100644
--- a/addons/l10n_ch/wizard/__init__.py
+++ b/addons/l10n_ch/wizard/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import setup_wizards
+from . import qr_invoice_wizard
diff --git a/addons/l10n_ch/wizard/qr_invoice_wizard.py b/addons/l10n_ch/wizard/qr_invoice_wizard.py
new file mode 100644
index 00000000000..4dfbf18e7b1
--- /dev/null
+++ b/addons/l10n_ch/wizard/qr_invoice_wizard.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+
+
+class QrInvoiceWizard(models.TransientModel):
+ '''
+ Wizard :
+ When multiple invoices are selected to be printed in the QR-Iban format,
+ this wizard will appear if one or more invoice(s) could not be QR-printed (wrong format...)
+ The user will then be able to print the invoices (in the format available, priority : QR --> ISR --> normal)
+ or to see a list of the non-QR/ISR invoices.
+ The non-QR/ISR invoices will have a note logged in their chatter, detailing the reason of the failure.
+ '''
+ _name = 'l10n_ch.qr_invoice.wizard'
+ _description = 'Handles problems occurring while creating multiple QR-invoices at once'
+
+ nb_qr_inv = fields.Integer(readonly=True)
+ nb_isr_inv = fields.Integer(readonly=True)
+ nb_classic_inv = fields.Integer(readonly=True)
+ qr_inv_text = fields.Text(readonly=True)
+ isr_inv_text = fields.Text(readonly=True)
+ classic_inv_text = fields.Text(readonly=True)
+
+ @api.model
+ def default_get(self, fields):
+ # Extends 'base'.
+
+ def determine_invoices_text(nb_inv, inv_format):
+ '''
+ Creates a sentence explaining nb_inv invoices could be printed in the inv_format format.
+ '''
+ if nb_inv == 0:
+ return _("No invoice could be printed in the %s format.", inv_format)
+ if nb_inv == 1:
+ return _("One invoice could be printed in the %s format.", inv_format)
+ return _("%s invoices could be printed in the %s format.", nb_inv, inv_format)
+
+ if not self._context.get('active_ids'):
+ raise UserError(_("No invoice was found to be printed."))
+
+ invoices = self.env['account.move'].browse(self._context['active_ids'])
+ companies = invoices.company_id
+ if len(companies) != 1 or companies[0].country_code != 'CH':
+ raise UserError(_("All selected invoices must belong to the same Switzerland company"))
+
+ results = super().default_get(fields)
+ dispatched_invoices = invoices._l10n_ch_dispatch_invoices_to_print()
+ results.update({
+ 'nb_qr_inv': len(dispatched_invoices['qr']),
+ 'nb_isr_inv': len(dispatched_invoices['isr']),
+ 'nb_classic_inv': len(dispatched_invoices['classic']),
+ 'qr_inv_text': determine_invoices_text(nb_inv=len(dispatched_invoices['qr']), inv_format="QR"),
+ 'isr_inv_text': determine_invoices_text(nb_inv=len(dispatched_invoices['isr']), inv_format="ISR"),
+ 'classic_inv_text': determine_invoices_text(nb_inv=len(dispatched_invoices['classic']),
+ inv_format="classic"),
+ })
+ return results
+
+ def print_all_invoices(self):
+ '''
+ Triggered by the Print All button
+ '''
+ all_invoices_ids = self.env.context.get('inv_ids')
+ return self.env.ref('account.account_invoices').report_action(all_invoices_ids)
+
+ def action_view_faulty_invoices(self):
+ '''
+ Open a list view of all the invoices that could not be printed in the QR nor the ISR format.
+ '''
+ # Prints the error stopping the invoice from being QR-printed in the invoice's chatter.
+ invoices = self.env['account.move'].browse(self._context['active_ids'])
+ dispatched_invoices = invoices._l10n_ch_dispatch_invoices_to_print()
+ faulty_invoices = dispatched_invoices['classic']
+
+ # Log a message inside the chatter explaining why the invoice is faulty.
+ for inv in faulty_invoices:
+ try:
+ # The error potentially raised in the following function helps create the wizard's message.
+ inv.partner_bank_id._eligible_for_qr_code('ch_qr', inv.partner_id, inv.currency_id, raises_error=True)
+ except UserError as e:
+ inv.message_post(body=e.name, message_type="comment")
+
+ action_vals = {
+ 'name': _("Invalid Invoices"),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.move',
+ 'context': {'create': False},
+ }
+ if len(faulty_invoices) == 1:
+ action_vals.update({
+ 'view_mode': 'form',
+ 'res_id': faulty_invoices.id,
+ })
+ else:
+ action_vals.update({
+ 'view_mode': 'tree',
+ 'domain': [('id', 'in', faulty_invoices.ids)],
+ })
+ return action_vals