[FIX] base,l10n_ch,account_qr_code_sepa,payment,websitesale: Display all QR codes in QR bill
Issue: When trying to print a Suisse QR bill, if multiple images are presents in document and they have a url as src, some pictures will not be displayed. (Same issue may occur with simple QR code) Cause: It's a known issue with wkhtmltopdf: https://github.com/odoo/odoo/commit/2949138a7d84cd6c925ea1745d62f25ef077bb8b Also, adding css class to body by js break wkhtmltopdf. Solution: Replace link by base64 image value (use a function to retrieve base64 image instead of image_url). Remove class 'l10n_ch_qr' added by js (no need since CSS file didacted to this report). Move `_get_qr_code_base64` and `_get_qr_code_url` logic/flow (since generic) to account module. Move specific logic like `_get_qr_vals` and `_get_qr_code_generation_params` to specific module (ex: l10n_ch). extra: Alter some css for better rendering + update unitest. opw-2620082 closes odoo/odoo#77643 X-original-commit: 699b6eeac993e3a8d97ae7949170f7e18ca05831 Signed-off-by: Olivier Colson <oco@odoo.com>
This commit is contained in:
@@ -3348,7 +3348,7 @@ class AccountMove(models.Model):
|
||||
return None
|
||||
|
||||
unstruct_ref = self.ref if self.ref else self.name
|
||||
rslt = self.partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
|
||||
rslt = self.partner_bank_id.build_qr_code_base64(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
|
||||
|
||||
# We only set qr_code_method after generating the url; otherwise, it
|
||||
# could be set even in case of a failure in the QR code generation
|
||||
|
||||
@@ -498,7 +498,7 @@ class AccountPayment(models.Model):
|
||||
and pay.currency_id:
|
||||
|
||||
if pay.partner_bank_id:
|
||||
qr_code = pay.partner_bank_id.build_qr_code_url(pay.amount, pay.ref, pay.ref, pay.currency_id, pay.partner_id)
|
||||
qr_code = pay.partner_bank_id.build_qr_code_base64(pay.amount, pay.ref, pay.ref, pay.currency_id, pay.partner_id)
|
||||
else:
|
||||
qr_code = None
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.image import image_data_uri
|
||||
|
||||
import werkzeug
|
||||
import werkzeug.exceptions
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
def build_qr_code_url(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
""" Returns the QR-code report URL to pay to this account with the given parameters,
|
||||
def _build_qr_code_vals(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
""" Returns the QR-code vals needed to generate the QR-code report link to pay this account with the given parameters,
|
||||
or None if no QR-code could be generated.
|
||||
|
||||
:param amount: The amount to be paid
|
||||
@@ -33,7 +38,14 @@ class ResPartnerBank(models.Model):
|
||||
error_message = self._check_for_qr_code_errors(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
if not error_message:
|
||||
return self._get_qr_code_url(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
return {
|
||||
'qr_method': candidate_method,
|
||||
'amount': amount,
|
||||
'currency': currency,
|
||||
'debtor_partner': debtor_partner,
|
||||
'free_communication': free_communication,
|
||||
'structured_communication': structured_communication,
|
||||
}
|
||||
|
||||
elif not silent_errors:
|
||||
error_header = _("The following error prevented '%s' QR-code to be generated though it was detected as eligible: ", candidate_name)
|
||||
@@ -41,6 +53,38 @@ class ResPartnerBank(models.Model):
|
||||
|
||||
return None
|
||||
|
||||
def build_qr_code_url(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
vals = self._build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
|
||||
if vals:
|
||||
return self._get_qr_code_url(
|
||||
vals['qr_method'],
|
||||
vals['amount'],
|
||||
vals['currency'],
|
||||
vals['debtor_partner'],
|
||||
vals['free_communication'],
|
||||
vals['structured_communication'],
|
||||
)
|
||||
return None
|
||||
|
||||
def build_qr_code_base64(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
|
||||
vals = self._build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
|
||||
if vals:
|
||||
return self._get_qr_code_base64(
|
||||
vals['qr_method'],
|
||||
vals['amount'],
|
||||
vals['currency'],
|
||||
vals['debtor_partner'],
|
||||
vals['free_communication'],
|
||||
vals['structured_communication']
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
return None
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
return None
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
""" Hook for extension, to support the different QR generation methods.
|
||||
This function uses the provided qr_method to try generation a QR-code for
|
||||
@@ -54,6 +98,36 @@ class ResPartnerBank(models.Model):
|
||||
:param free_communication: Free communication to add to the payment when generating one with the QR-code
|
||||
:param structured_communication: Structured communication to add to the payment when generating one with the QR-code
|
||||
"""
|
||||
# pylint: disable=E1137
|
||||
# (PyLint doesn't get that we are not assigning to None here)
|
||||
params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
if params:
|
||||
params['type'] = params.pop('barcode_type')
|
||||
return '/report/barcode/?' + werkzeug.urls.url_encode(params)
|
||||
return None
|
||||
|
||||
def _get_qr_code_base64(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
""" Hook for extension, to support the different QR generation methods.
|
||||
This function uses the provided qr_method to try generation a QR-code for
|
||||
the given data. It it succeeds, it returns QR code in base64 url; else None.
|
||||
|
||||
:param qr_method: The QR generation method to be used to make the QR-code.
|
||||
:param amount: The amount to be paid
|
||||
:param currency: The currency in which amount is expressed
|
||||
:param debtor_partner: The partner to which this QR-code is aimed (so the one who will have to pay)
|
||||
:param free_communication: Free communication to add to the payment when generating one with the QR-code
|
||||
:param structured_communication: Structured communication to add to the payment when generating one with the QR-code
|
||||
"""
|
||||
# pylint: disable=E1137
|
||||
# pylint: disable=E1134
|
||||
# (PyLint doesn't get that we are not assigning to None here)
|
||||
params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
if params:
|
||||
try:
|
||||
barcode = self.env['ir.actions.report'].barcode(**params)
|
||||
except (ValueError, AttributeError):
|
||||
raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.')
|
||||
return image_data_uri(base64.b64encode(barcode))
|
||||
return None
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
import werkzeug
|
||||
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = 'res.partner.bank'
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'sct_qr':
|
||||
|
||||
comment = (free_communication or '') if not structured_communication else ''
|
||||
|
||||
qr_code_vals = [
|
||||
@@ -27,10 +24,19 @@ class ResPartnerBank(models.Model):
|
||||
comment[:141], # Remittance Information (Unstructured) (can't be set if there is a structured one)
|
||||
'', # Beneficiary to Originator Information
|
||||
]
|
||||
return qr_code_vals
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
return '/report/barcode/?' + werkzeug.urls.url_encode({'type': 'QR', 'value': '\n'.join(qr_code_vals), 'width': 128, 'height': 128, 'humanreadable': 1})
|
||||
|
||||
return super()._get_qr_code_url(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'sct_qr':
|
||||
return {
|
||||
'barcode_type': 'QR',
|
||||
'width': 128,
|
||||
'height': 128,
|
||||
'humanreadable': 1,
|
||||
'value': '\n'.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
|
||||
}
|
||||
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):
|
||||
if qr_method == 'sct_qr':
|
||||
|
||||
@@ -9,7 +9,6 @@ from odoo.addons.base_iban.models.res_partner_bank import normalize_iban, pretty
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import mod10r
|
||||
|
||||
import werkzeug.urls
|
||||
|
||||
ISR_SUBSCRIPTION_CODE = {'CHF': '01', 'EUR': '03'}
|
||||
CLEARING = "09000"
|
||||
@@ -236,14 +235,6 @@ class ResPartnerBank(models.Model):
|
||||
return self._pretty_postal_num(iban[-9:])
|
||||
return None
|
||||
|
||||
def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
qr_code_vals = self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
return '/report/barcode/?type=%s&value=%s&width=%s&height=%s&quiet=1&mask=ch_cross' % ('QR', werkzeug.urls.url_quote_plus('\n'.join(qr_code_vals)), 256, 256)
|
||||
|
||||
return super()._get_qr_code_url(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _l10n_ch_get_qr_vals(self, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
comment = ""
|
||||
if free_communication:
|
||||
@@ -301,6 +292,23 @@ class ResPartnerBank(models.Model):
|
||||
'EPD', # Mandatory trailer part
|
||||
]
|
||||
|
||||
def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||
if qr_method == 'ch_qr':
|
||||
return {
|
||||
'barcode_type': 'QR',
|
||||
'width': 256,
|
||||
'height': 256,
|
||||
'quiet': 1,
|
||||
'mask': 'ch_cross',
|
||||
'value': '\n'.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
|
||||
}
|
||||
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ class ReportSwissQR(models.AbstractModel):
|
||||
|
||||
qr_code_urls = {}
|
||||
for invoice in docs:
|
||||
qr_code_urls[invoice.id] = invoice.partner_bank_id.build_qr_code_url(invoice.amount_residual, invoice.ref or invoice.name, invoice.payment_reference, invoice.currency_id, invoice.partner_id, qr_method='ch_qr', silent_errors=False)
|
||||
qr_code_urls[invoice.id] = invoice.partner_bank_id.build_qr_code_base64(invoice.amount_residual, invoice.ref or invoice.name, invoice.payment_reference, invoice.currency_id, invoice.partner_id, qr_method='ch_qr', silent_errors=False)
|
||||
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
<template id="l10n_ch_swissqr_template">
|
||||
<t t-set="o" t-value="o.with_context(lang=lang)"/>
|
||||
<t t-call="web.external_layout">
|
||||
<!-- add class to body tag -->
|
||||
<script>document.body.className += " l10n_ch_qr";</script>
|
||||
<!-- add default margin for header (matching A4 European margin) -->
|
||||
<t t-set="report_header_style">padding-top:6.2mm; padding-left:8.2mm; padding-right:8.2mm;</t>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
body.l10n_ch_qr {
|
||||
padding:0;
|
||||
body {
|
||||
padding: 0!important;
|
||||
|
||||
/* Disable custom bakground */
|
||||
.o_report_layout_striped {
|
||||
@@ -10,7 +10,7 @@ body.l10n_ch_qr {
|
||||
.swissqr_title {
|
||||
position: absolute;
|
||||
padding: 15px;
|
||||
padding-top: 150px;
|
||||
padding-top: 200px;
|
||||
}
|
||||
|
||||
.swissqr_content {
|
||||
|
||||
@@ -99,7 +99,8 @@ class TestSwissQR(AccountTestInvoicingCommon):
|
||||
)
|
||||
|
||||
def swissqr_generated(self, invoice, ref_type='NON'):
|
||||
""" Prints the given invoice and tests that a Swiss QR generation is triggered. """
|
||||
""" Ensure correct params for Swiss QR generation. """
|
||||
|
||||
self.assertTrue(
|
||||
invoice.partner_bank_id._eligible_for_qr_code('ch_qr', invoice.partner_id, invoice.currency_id), 'A Swiss QR can be generated'
|
||||
)
|
||||
@@ -111,31 +112,31 @@ class TestSwissQR(AccountTestInvoicingCommon):
|
||||
else:
|
||||
struct_ref = ''
|
||||
unstr_msg = invoice.payment_reference or invoice.ref or invoice.name or ''
|
||||
unstr_msg = (unstr_msg or invoice.number).replace('/', '%2F')
|
||||
unstr_msg = unstr_msg or invoice.number
|
||||
|
||||
payload = (
|
||||
"SPC%0A"
|
||||
"0200%0A"
|
||||
"1%0A"
|
||||
"{iban}%0A"
|
||||
"K%0A"
|
||||
"company_1_data%0A"
|
||||
"Route+de+Berne+88%0A"
|
||||
"2000+Neuch%C3%A2tel%0A"
|
||||
"%0A%0A"
|
||||
"CH%0A"
|
||||
"%0A%0A%0A%0A%0A%0A%0A"
|
||||
"42.00%0A"
|
||||
"CHF%0A"
|
||||
"K%0A"
|
||||
"Partner%0A"
|
||||
"Route+de+Berne+41%0A"
|
||||
"1000+Lausanne%0A"
|
||||
"%0A%0A"
|
||||
"CH%0A"
|
||||
"{ref_type}%0A"
|
||||
"{struct_ref}%0A"
|
||||
"{unstr_msg}%0A"
|
||||
"SPC\n"
|
||||
"0200\n"
|
||||
"1\n"
|
||||
"{iban}\n"
|
||||
"K\n"
|
||||
"company_1_data\n"
|
||||
"Route de Berne 88\n"
|
||||
"2000 Neuchâtel\n"
|
||||
"\n\n"
|
||||
"CH\n"
|
||||
"\n\n\n\n\n\n\n"
|
||||
"42.00\n"
|
||||
"CHF\n"
|
||||
"K\n"
|
||||
"Partner\n"
|
||||
"Route de Berne 41\n"
|
||||
"1000 Lausanne\n"
|
||||
"\n\n"
|
||||
"CH\n"
|
||||
"{ref_type}\n"
|
||||
"{struct_ref}\n"
|
||||
"{unstr_msg}\n"
|
||||
"EPD"
|
||||
).format(
|
||||
iban=invoice.partner_bank_id.sanitized_acc_number,
|
||||
@@ -144,11 +145,20 @@ class TestSwissQR(AccountTestInvoicingCommon):
|
||||
unstr_msg=unstr_msg,
|
||||
)
|
||||
|
||||
expected_url = ("/report/barcode/?type=QR&value={}"
|
||||
"&width=256&height=256&quiet=1&mask=ch_cross").format(payload)
|
||||
expected_params = {
|
||||
'barcode_type': 'QR',
|
||||
'width': 256,
|
||||
'height': 256,
|
||||
'quiet': 1,
|
||||
'mask': 'ch_cross',
|
||||
'value': payload,
|
||||
}
|
||||
|
||||
url = invoice.generate_qr_code()
|
||||
self.assertEqual(url, expected_url)
|
||||
params = invoice.partner_bank_id._get_qr_code_generation_params(
|
||||
'ch_qr', 42.0, invoice.currency_id, invoice.partner_id, unstr_msg, struct_ref
|
||||
)
|
||||
|
||||
self.assertEqual(params, expected_params)
|
||||
|
||||
def test_swissQR_missing_bank(self):
|
||||
# Let us test the generation of a SwissQR for an invoice, first by showing an
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<strong>Communication: </strong><span t-esc="tx.reference"/>
|
||||
</div>
|
||||
<div t-if="tx.acquirer_id.sudo().qr_code">
|
||||
<t t-set="qr_code" t-value="tx.acquirer_id.sudo().journal_id.bank_account_id.build_qr_code_url(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"/>
|
||||
<t t-set="qr_code" t-value="tx.acquirer_id.sudo().journal_id.bank_account_id.build_qr_code_base64(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"/>
|
||||
<div t-if="qr_code" class="mt-2">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code"/>
|
||||
|
||||
@@ -1840,7 +1840,7 @@
|
||||
<b>Communication: </b><span t-esc='order.reference'/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id.acquirer_id.sudo().qr_code">
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.sudo().journal_id.bank_account_id.build_qr_code_url(order.amount_total,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.sudo().journal_id.bank_account_id.build_qr_code_base64(order.amount_total,payment_tx_id.reference, None, payment_tx_id.currency_id, payment_tx_id.partner_id)"/>
|
||||
<div class="card-body" t-if="qr_code">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code"/>
|
||||
|
||||
@@ -6,9 +6,6 @@ from collections.abc import Iterable
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
import werkzeug.urls
|
||||
|
||||
def sanitize_account_number(acc_number):
|
||||
if acc_number:
|
||||
|
||||
Reference in New Issue
Block a user