[IMP][l10n_ch] account: allow for QR bills to be printed in batch
The only option to print a QR Invoice was to go on the invoice page and to click on the Print QR Button. This PR allows for a user to print multiple QR codes, selected from the Invoices view. The download occurs normally if all invoices are valid and QR-printable. A wizard opens when the whole invoice selection isn't valid. It allows to download the invoices as a single pdf or to see a list of the ones that could not be printed in the QR format. The wizard's text depends on the number of QR-valid, ISR-valid and classic (non QR, non ISR) invoices that the user tried to print. All invoices for which a print is asked will be printed with a QR bill if it is possible. If not, this will check if printing in the ISR format is possible. Lastly, if none is possible, the classic invoice will be printed. The behaviour allowed for the removal of the QR PRINT and ISR PRINT buttons on the single invoice view, for a behaviour closer to the original guidelines. Bills can't be QR printed. Corrected QR-Bill CSS. task-2726507 closes odoo/odoo#82341 Signed-off-by: Laurent Smet <las@odoo.com>
This commit is contained in:
@@ -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
|
||||
return None
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<field name="report_file">l10n_ch.qr_report_main</field>
|
||||
<field name="print_report_name">'QR-bill-%s' % object.name</field>
|
||||
<field name="paperformat_id" ref="l10n_ch.paperformat_euro_no_margin"/>
|
||||
<field name="attachment">'QR-bill-' + object.name + '.pdf'</field>
|
||||
</record>
|
||||
|
||||
<template id="l10n_ch_swissqr_template">
|
||||
|
||||
@@ -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
|
||||
|
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -9,58 +9,7 @@
|
||||
<xpath expr="//field[@name='is_move_sent']" position="after">
|
||||
<field name="l10n_ch_isr_sent" invisible="1"/>
|
||||
<field name="l10n_ch_currency_name" invisible="1" readonly="1"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[@id='account_invoice_payment_btn']" position="before">
|
||||
<button id="l10n_ch_btn_isr_print_highlight"
|
||||
name="isr_print"
|
||||
string="Print ISR"
|
||||
type="object"
|
||||
attrs="{'invisible':['|', '|', '|', '|',
|
||||
('move_type', 'not in', ('out_invoice', 'out_refund')),
|
||||
('l10n_ch_isr_sent', '=', True),
|
||||
('state', '!=', 'posted'),
|
||||
('payment_state', 'not in', ('not_paid', 'partial')),
|
||||
('l10n_ch_currency_name', 'not in', ['EUR', 'CHF'])]}"
|
||||
class="oe_highlight"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[@id='account_invoice_payment_btn']" position="before">
|
||||
<button id="btn_isr_print_normal"
|
||||
name="isr_print"
|
||||
string="Print ISR"
|
||||
type="object"
|
||||
attrs="{'invisible':['|', '|', '|', '|',
|
||||
('move_type', 'not in', ('out_invoice', 'out_refund')),
|
||||
('l10n_ch_isr_sent', '=', False),
|
||||
('state', '!=', 'posted'),
|
||||
('payment_state', 'not in', ('not_paid', 'partial')),
|
||||
('l10n_ch_currency_name', 'not in', ['EUR', 'CHF'])]}"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
<button
|
||||
name="print_ch_qr_bill"
|
||||
string="Print QR-bill"
|
||||
type="object"
|
||||
attrs="{'invisible':['|', ('state', '!=', 'posted'),
|
||||
'|', ('l10n_ch_isr_sent', '=', True),
|
||||
'|', ('move_type', '!=', 'out_invoice'),
|
||||
('l10n_ch_currency_name', 'not in', ['EUR', 'CHF'])]}"
|
||||
groups="base.group_user"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button
|
||||
name="print_ch_qr_bill"
|
||||
string="Print QR-bill"
|
||||
type="object"
|
||||
attrs="{'invisible':['|', ('state', '!=', 'posted'),
|
||||
'|', ('l10n_ch_isr_sent', '=', False),
|
||||
'|', ('move_type', '!=', 'out_invoice'),
|
||||
('l10n_ch_currency_name', 'not in', ['EUR', 'CHF'])]}"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
<field name="l10n_ch_is_qr_valid" invisible="1"/>
|
||||
</xpath>
|
||||
<header position="after">
|
||||
<field name="l10n_ch_isr_needs_fixing" invisible="1"/>
|
||||
@@ -90,5 +39,17 @@
|
||||
<field name="res_model">account.move</field>
|
||||
<field name="search_view_id" ref="isr_invoice_search_view"/>
|
||||
</record>
|
||||
|
||||
<record id="l10n_ch_qr_server_action" model="ir.actions.server">
|
||||
<field name="name">Print QR Invoices</field>
|
||||
<field name="model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.l10n_ch_action_print_qr()
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="l10n_ch_qr_invoice_wizard_form" model="ir.ui.view">
|
||||
<field name="name">l10n_ch.qr_invoice.wizard.form</field>
|
||||
<field name="model">l10n_ch.qr_invoice.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="QR printing encountered a problem">
|
||||
<field name='nb_qr_inv' invisible="1"/>
|
||||
<field name="nb_classic_inv" invisible="1"/>
|
||||
<field name="nb_isr_inv" invisible="1"/>
|
||||
<p>
|
||||
<field name="qr_inv_text"/>
|
||||
<field name="isr_inv_text"/>
|
||||
<field name="classic_inv_text"/>
|
||||
</p>
|
||||
<p>
|
||||
To be able to print all invoices in the QR format, you might need to : <br/>
|
||||
- check the account is a valid QR-IBAN<br/>
|
||||
- or check your company and the partners are located in Switzerland.<br/>
|
||||
Press Check Invalid Invoices to see a list of the invoices that were printed without an ISR or a QR.
|
||||
</p>
|
||||
<footer>
|
||||
<button name="print_all_invoices" string="Print All" type="object" class="btn-primary"/>
|
||||
<button name="action_view_faulty_invoices" string="Check invalid invoices" type="object"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_ch_qr_invoice_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Qr Batch error Wizard</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">l10n_ch.qr_invoice.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="l10n_ch_qr_invoice_wizard_form"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import setup_wizards
|
||||
from . import qr_invoice_wizard
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user