[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:
Nasreddin Boulif (bon)
2021-11-08 09:25:47 +00:00
parent 1a42bd68a9
commit 0e50113f69
12 changed files with 153 additions and 60 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+77 -3
View File
@@ -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
+13 -7
View File
@@ -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':
+17 -9
View File
@@ -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
+1 -1
View File
@@ -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,
-2
View File
@@ -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 {
+38 -28
View File
@@ -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"/>
+1 -1
View File
@@ -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"/>
-3
View File
@@ -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: