[IMP] base, account, l10n_ch, website_sale, payment, web: make payment QR-code generation more generic

This new modelling makes it easier to add new QR-code formats, and allows using all of them in website_sale and account.payment's form view as well (so, Swiss QR codes are now available there, while they were restricted to only invoices in the past). All barcodes are now generated as reports, from a dedicated route. This was only partly the case before : Swiss QR added a cross on top of the QR-code directly in the template, it wasn't part of the image returned by the route; now it is.

[ADD] base_qr_code_sepa: new module decoupling SEPA QR-codes generation from the base module

Each new QR-code generation option should thus be done in a dedicated module (or added to a localization) in the future.

[IMP] base_qr_code_sepa: update the generated QR codes to version 2 of the specification

Version 1 is still supported, so no need to backport this.

[IMP] l10n_ch: make Swiss QR-codes compatible with the new version of the specification (the old one is deprecated)

This will be backported to 11.0 and 12.0, as these QR-codes will soon replace ISR.

[IMP] account: make it possible to mark manual payments as sent with a button on the form view

This way, when making them directly with a QR-code (or doing a more classical wire transfer), people can keep track of what they already have asked the bank to do, and what they still have to treat.

closes odoo/odoo#44839

Related: odoo/enterprise#8262
Related: odoo/upgrade#992
Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
oco-odoo
2020-03-30 11:28:41 +00:00
parent c1f3596bd3
commit 0eef278268
33 changed files with 656 additions and 202 deletions
+1
View File
@@ -23,3 +23,4 @@ from . import digest
from . import res_users
from . import ir_actions_report
from . import res_currency
from . import res_bank
+43
View File
@@ -242,6 +242,10 @@ class AccountMove(models.Model):
invoice_incoterm_id = fields.Many2one('account.incoterms', string='Incoterm',
default=_get_default_invoice_incoterm,
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
display_qr_code = fields.Boolean(string="Display QR-code", related='company_id.qr_code')
qr_code_method = fields.Selection(string="Payment QR-code",
selection=lambda self: self.env['res.partner.bank'].get_available_qr_methods_in_sequence(),
help="Type of QR-code to be generated for the payment of this invoice, when printing it. If left blank, the first available and usable method will be used.")
# ==== Payment widget fields ====
invoice_outstanding_credits_debits_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
@@ -2469,6 +2473,45 @@ class AccountMove(models.Model):
line[2]['credit'] = currency_id and formatLang(self.env, line[2]['credit'], currency_obj=currency_id) or line[2]['debit']
return preview_vals
def generate_qr_code(self):
""" Generates and returns a QR-code generation URL for this invoice,
raising an error message if something is misconfigured.
The chosen QR generation method is the one set in qr_method field if there is one,
or the first eligible one found. If this search had to be performed and
and eligible method was found, qr_method field is set to this method before
returning the URL. If no eligible QR method could be found, we return None.
"""
self.ensure_one()
if not self.is_invoice():
raise UserError(_("QR-codes can only be generated for invoice entries."))
qr_code_method = self.qr_code_method
if qr_code_method:
# If the user set a qr code generator manually, we check that we can use it
if not self.invoice_partner_bank_id._eligible_for_qr_code(self.qr_code_method, self.partner_id, self.currency_id):
raise UserError(_("The chosen QR-code type is not eligible for this invoice."))
else:
# Else we find one that's eligible and assign it to the invoice
for candidate_method, candidate_name in self.env['res.partner.bank'].get_available_qr_methods_in_sequence():
if self.invoice_partner_bank_id._eligible_for_qr_code(candidate_method, self.partner_id, self.currency_id):
qr_code_method = candidate_method
break
if not qr_code_method:
# No eligible method could be found; we can't generate the QR-code
return None
unstruct_ref = self.ref if self.ref else self.name
rslt = self.invoice_partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.invoice_payment_ref, 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
# (which would change the field, but not refresh UI, making the displayed data inconsistent with db)
self.qr_code_method = qr_code_method
return rslt
class AccountMoveLine(models.Model):
_name = "account.move.line"
+30 -1
View File
@@ -94,6 +94,7 @@ class account_payment(models.Model):
possible_bank_partner_ids = fields.Many2many('res.partner', compute='_compute_possible_bank_partners')
show_partner_bank_account = fields.Boolean(compute='_compute_show_partner_bank', help='Technical field used to know whether the field `partner_bank_account_id` needs to be displayed or not in the payments form views')
require_partner_bank_account = fields.Boolean(compute='_compute_show_partner_bank', help='Technical field used to know whether the field `partner_bank_account_id` needs to be required or not in the payments form views')
qr_code = fields.Char(string="QR Code", compute="_compute_qr_code", help="QR-code report URL to use to generate the QR-code to scan with a banking app to perform this payment.")
@api.model
def default_get(self, default_fields):
@@ -171,6 +172,28 @@ class account_payment(models.Model):
payment_methods = p.journal_id.outbound_payment_method_ids
p._payment_methods = default | payment_methods
@api.depends('partner_bank_account_id', 'amount', 'communication', 'currency_id', 'journal_id', 'state', 'payment_method_id', 'payment_type')
def _compute_qr_code(self):
for record in self:
if record.state in ('draft', 'posted') \
and record.partner_bank_account_id \
and record.payment_method_id.code == 'manual' \
and record.payment_type == 'outbound':
currency = record.currency_id or record.company_id.currency_id
qr_code = record.partner_bank_account_id and record.partner_bank_account_id.build_qr_code_url(record.amount, record.communication, None, currency, record.partner_id) or None
if qr_code:
record.qr_code = '''
<br/>
<img class="border border-dark rounded" src="{qr_code}"/>
<br/>
<strong class="text-center">{txt}</strong>
'''.format(txt = _('Scan me with your banking app.'),
qr_code = qr_code)
continue
record.qr_code = None
@api.constrains('amount')
def _check_amount(self):
for payment in self:
@@ -179,7 +202,7 @@ class account_payment(models.Model):
@api.model
def _get_method_codes_using_bank_account(self):
return []
return ['manual']
@api.model
def _get_method_codes_needing_bank_account(self):
@@ -414,6 +437,12 @@ class account_payment(models.Model):
'context': {'create': False},
}
def mark_as_sent(self):
self.write({'state': 'sent'})
def unmark_as_sent(self):
self.write({'state': 'posted'})
def unreconcile(self):
""" Set back the payments in 'posted' or 'sent' state, without deleting the journal entries.
Called when cancelling a bank statement line linked to a pre-registered payment.
+1 -1
View File
@@ -71,7 +71,7 @@ class ResCompany(models.Model):
incoterm_id = fields.Many2one('account.incoterms', string='Default incoterm',
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
qr_code = fields.Boolean(string='Display SEPA QR code')
qr_code = fields.Boolean(string='Display QR-code on invoices')
invoice_is_email = fields.Boolean('Email by default', default=True)
invoice_is_print = fields.Boolean('Print by default', default=True)
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from odoo import api, models, fields, _
from odoo.exceptions import UserError
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,
or None if no QR-code could be generated.
:param amount: The amount to be paid
: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
: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 qr_method: The QR generation method to be used to make the QR-code. If None, the first one giving a result will be used.
:param silent_errors: If true, forbids errors to be raised if some tested QR-code format can't be generated because of incorrect data.
"""
if not self:
return None
self.ensure_one()
if not currency:
raise UserError(_("Currency must always be provided in order to generate a QR-code"))
available_qr_methods = self.get_available_qr_methods_in_sequence()
candidate_methods = qr_method and [(qr_method, dict(available_qr_methods)[qr_method])] or available_qr_methods
for candidate_method, candidate_name in candidate_methods:
if self._eligible_for_qr_code(candidate_method, debtor_partner, currency):
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)
elif not silent_errors:
error_header = _("The following error prevented '%s' QR-code to be generated though it was detected as eligible: ") % candidate_name
raise UserError( error_header + error_message)
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
the given data. It it succeeds, it returns the report URL to make this
QR-code; 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
"""
return None
@api.model
def _get_available_qr_methods(self):
""" Returns the QR-code generation methods that are available on this db,
in the form of a list of (code, name, sequence) elements, where
'code' is a unique string identifier, 'name' the name to display
to the user to designate the method, and 'sequence' is a positive integer
indicating the order in which those mehtods need to be checked, to avoid
shadowing between them (lower sequence means more prioritary).
"""
return []
@api.model
def get_available_qr_methods_in_sequence(self):
""" Same as _get_available_qr_methods but without returning the sequence,
and using it directly to order the returned list.
"""
all_available = self._get_available_qr_methods()
all_available.sort(key=lambda x: x[2])
return [(code, name) for (code, name, sequence) in all_available]
def _eligible_for_qr_code(self, qr_method, debtor_partner, currency):
""" 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,
it only checks that this type of QR-code *should be* possible to generate.
Consistency of the required field needs then to be checked by _check_for_qr_code_errors().
"""
return False
def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
""" Checks the data before generating a QR-code for the specified qr_method
(this method must have been checked for eligbility by _eligible_for_qr_code() first).
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
+1 -1
View File
@@ -80,7 +80,7 @@ class ResConfigSettings(models.TransientModel):
This is useful if you install accounting after having used invoicing for some time and
don't want to reconcile all the past payments with bank statements.""")
qr_code = fields.Boolean(string='Display SEPA QR code', related='company_id.qr_code', readonly=False)
qr_code = fields.Boolean(string='Display SEPA QR-code', related='company_id.qr_code', readonly=False)
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False)
invoice_is_email = fields.Boolean(string='Send Email', related='company_id.invoice_is_email', readonly=False)
incoterm_id = fields.Many2one('account.incoterms', string='Default incoterm', related='company_id.incoterm_id', help='International Commercial Terms are a series of predefined commercial terms used in international transactions.', readonly=False)
@@ -195,15 +195,35 @@ class AccountInvoiceReport(models.Model):
))
class ReportInvoiceWithPayment(models.AbstractModel):
_name = 'report.account.report_invoice_with_payments'
_description = 'Account report with payment lines'
class ReportInvoiceWithoutPayment(models.AbstractModel):
_name = 'report.account.report_invoice'
_description = 'Account report without payment lines'
@api.model
def _get_report_values(self, docids, data=None):
docs = self.env['account.move'].browse(docids)
qr_code_urls = {}
for invoice in docs:
if invoice.display_qr_code:
new_code_url = invoice.generate_qr_code()
if new_code_url:
qr_code_urls[invoice.id] = new_code_url
return {
'doc_ids': docids,
'doc_model': 'account.move',
'docs': self.env['account.move'].browse(docids),
'report_type': data.get('report_type') if data else '',
'docs': docs,
'qr_code_urls': qr_code_urls,
}
class ReportInvoiceWithPayment(models.AbstractModel):
_name = 'report.account.report_invoice_with_payments'
_description = 'Account report with payment lines'
_inherit = 'report.account.report_invoice'
@api.model
def _get_report_values(self, docids, data=None):
rslt = super()._get_report_values(docids, data)
rslt['report_type'] = data.get('report_type') if data else ''
return rslt
@@ -571,6 +571,7 @@
<field name="company_currency_id" invisible="1"/>
<field name="commercial_partner_id" invisible="1"/>
<field name="bank_partner_id" invisible="1"/>
<field name="display_qr_code" invisible="1"/>
<field name="invoice_has_outstanding" invisible="1"/>
<field name="invoice_sent" invisible="1"/>
@@ -1001,6 +1002,7 @@
context="{'default_partner_id': commercial_partner_id}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="qr_code_method" attrs="{'invisible': [('display_qr_code', '=', False)]}"/>
</group>
</group>
</page>
@@ -128,6 +128,8 @@
<button name="post" class="oe_highlight" states="draft" string="Confirm" type="object"/>
<button name="action_draft" class="btn btn-secondary" attrs="{'invisible': [('state', '=', 'draft')]}" string="Reset To Draft" type="object" groups="account.group_account_invoice"/>
<button name="cancel" attrs="{'invisible': [('state', 'in', ('cancelled', 'posted', 'invoicing_legacy'))]}" string="Cancel" type="object"/>
<button name="mark_as_sent" string="Mark as Sent" type="object" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('payment_method_code', '!=', 'manual')]}"/>
<button name="unmark_as_sent" string="Unmark as Sent" type="object" attrs="{'invisible': ['|', ('state', '!=', 'sent'), ('payment_method_code', '!=', 'manual')]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted,reconciled,cancelled"/>
</header>
@@ -202,6 +204,13 @@
</div>
</div>
</group>
<group>
<field name="qr_code" invisible="1"/>
<div attrs="{'invisible': [('qr_code', '=', False)]}" colspan="2" class="text-center">
<field name="qr_code" widget="html"/>
</div>
</group>
</group>
</sheet>
<div class="o_attachment_preview"/>
+4 -7
View File
@@ -177,13 +177,10 @@
<p t-if="o.invoice_incoterm_id" name="note">
<strong>Incoterm: </strong><span t-field="o.invoice_incoterm_id.code"/> - <span t-field="o.invoice_incoterm_id.name"/>
</p>
<div id="qrcode" t-if="(o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.invoice_partner_bank_id.acc_number != False)">
<p t-if="(o.invoice_partner_bank_id.qr_code_valid)">
<strong class="text-center">Scan me with your banking app.</strong><br /><br />
<img class="border border-dark rounded" t-att-src="o.invoice_partner_bank_id.build_qr_code_url(o.amount_residual,(o.ref) if (o.ref) else o.name)"/>
</p>
<p t-if="(o.invoice_partner_bank_id.qr_code_valid == False)">
<strong class="text-center">The SEPA QR Code informations are not set correctly.</strong><br />
<div id="qrcode" t-if="o.display_qr_code">
<p t-if="qr_code_urls.get(o.id)">
<strong class="text-center">Scan me with your banking app.</strong><br/><br/>
<img class="border border-dark rounded" t-att-src="qr_code_urls[o.id]"/>
</p>
</div>
</div>
@@ -356,15 +356,15 @@
</div>
<div class="col-xs-12 col-md-6 o_setting_box"
id="qr_code_invoices"
title="Add an EPC QR code to your invoices so that your customers can pay instantly with their mobile banking application. EPC QR codes are used by many European banks to process SEPA payments.">
title="Add a QR-code to your invoices so that your customers can pay instantly with their mobile banking application.">
<div class="o_setting_left_pane">
<field name="qr_code" class="oe_inline" widget="upgrade_boolean"/>
<field name="qr_code" class="oe_inline"/>
</div>
<div class="o_setting_right_pane" name="qr_code_right_pane">
<label string="SEPA QR Code" for="qr_code"/>
<label string="QR Codes" for="qr_code"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Add a payment QR code to your invoices
Add a payment QR-code to your invoices
</div>
</div>
</div>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import tests
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': "base_qr_code_sepa",
'description': """
This module adds support for SEPA Credit Transfer QR-code generation.
""",
'category': 'Accounting/Payment',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['account'],
'data': [
],
'auto_install': True,
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import res_bank
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
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):
if qr_method == 'sct_qr':
comment = (free_communication or '') if not structured_communication else ''
qr_code_vals = [
'BCD', # Service Tag
'002', # Version
'1', # Character Set
'SCT', # Identification Code
self.bank_bic or '', # BIC of the Beneficiary Bank
(self.acc_holder_name or self.partner_id.name)[:71], # Name of the Beneficiary
self.sanitized_acc_number, # Account Number of the Beneficiary
str(amount), # Amount of the Transfer in EUR
'', # Purpose of the Transfer
(structured_communication or '')[:36], # Remittance Information (Structured)
comment[:141], # Remittance Information (Unstructured) (can't be set if there is a structured one)
'', # Beneficiary to Originator Information
]
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 _eligible_for_qr_code(self, qr_method, debtor_partner, currency):
if qr_method == 'sct_qr':
# Some countries share the same IBAN country code
# (e.g. Åland Islands and Finland IBANs are 'FI', but Åland Islands' code is 'AX').
sepa_country_codes = self.env.ref('base.sepa_zone').country_ids.mapped('code')
non_iban_codes = {'AX', 'NC', 'YT', 'TF', 'BL', 'RE', 'MF', 'GP', 'PM', 'PF', 'GF', 'MQ', 'JE', 'GG', 'IM'}
sepa_iban_codes = {code for code in sepa_country_codes if code not in non_iban_codes}
return currency.name == 'EUR' and self.acc_type == 'iban' and self.sanitized_acc_number[:2] in sepa_iban_codes
return super()._eligible_for_qr_code(qr_method, debtor_partner, currency)
def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
if qr_method == 'sct_qr':
if not self.acc_holder_name and not self.partner_id.name:
return _("The account receiving the payment must have an account horder name or partner name set.")
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
@api.model
def _get_available_qr_methods(self):
rslt = super()._get_available_qr_methods()
rslt.append(('sct_qr', _("SEPA Credit Transfer QR"), 20))
return rslt
@@ -0,0 +1,3 @@
# *-* coding:utf-8 *-*
from . import test_sepa_qr
@@ -0,0 +1,62 @@
# -*- coding:utf-8 -*-
from odoo.exceptions import UserError
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestSEPAQRCode(AccountTestInvoicingCommon):
""" Tests the generation of Swiss QR-codes on invoices
"""
@classmethod
def setUpClass(cls):
super(TestSEPAQRCode, cls).setUpClass()
cls.company_data['company'].qr_code = True
cls.acc_sepa_iban = cls.env['res.partner.bank'].create({
'acc_number': 'BE15001559627230',
'partner_id': cls.company_data['company'].partner_id.id,
})
cls.acc_non_sepa_iban = cls.env['res.partner.bank'].create({
'acc_number': 'SA4420000001234567891234',
'partner_id': cls.company_data['company'].partner_id.id,
})
cls.sepa_qr_invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'currency_id': cls.env.ref('base.EUR').id,
'invoice_partner_bank_id': cls.acc_sepa_iban.id,
'company_id': cls.company_data['company'].id,
'invoice_line_ids': [
(0, 0, {'quantity': 1, 'price_unit': 100})
],
})
def test_sepa_qr_code_generation(self):
""" Check different cases of SEPA QR-code generation, when qr_method is
specified beforehand.
"""
self.sepa_qr_invoice.qr_code_method = 'sct_qr'
# Using a SEPA IBAN should work
self.sepa_qr_invoice.generate_qr_code()
# Using a non-SEPA IBAN shouldn't
self.sepa_qr_invoice.invoice_partner_bank_id = self.acc_non_sepa_iban
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a SEPA QR-code for IBAN of countries outside SEPA zone."):
self.sepa_qr_invoice.generate_qr_code()
# Changing the currency should break it as well
self.sepa_qr_invoice.invoice_partner_bank_id = self.acc_sepa_iban
self.sepa_qr_invoice.currency_id = self.env.ref('base.USD').id
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a SEPA QR-code for another currency as EUR."):
self.sepa_qr_invoice.generate_qr_code()
def test_sepa_qr_code_detection(self):
""" Checks SEPA QR-code auto-detection when no specific QR-method
is given to the invoice.
"""
self.sepa_qr_invoice.generate_qr_code()
self.assertEqual(self.sepa_qr_invoice.qr_code_method, 'sct_qr', "SEPA QR-code generator should have been chosen for this invoice.")
+2 -1
View File
@@ -7,4 +7,5 @@ from . import account_journal
from . import mail_template
from . import res_bank
from . import res_company
from . import account_bank_statement
from . import account_bank_statement
from . import ir_actions_report
+4 -3
View File
@@ -166,11 +166,12 @@ class AccountMove(models.Model):
return float_split_str(self.amount_residual, 2)
def display_swiss_qr_code(self):
""" Trigger the print of the Swiss QR code in the invoice report or not
""" Returns whether or not to print the QR-bills additional page when
generation an invoice pdf.
"""
self.ensure_one()
qr_parameter = self.env['ir.config_parameter'].sudo().get_param('l10n_ch.print_qrcode')
return self.partner_id.country_id.code == 'CH' and qr_parameter
return self.display_qr_code and \
self.invoice_partner_bank_id._eligible_for_qr_code('ch_qr', self.partner_id, self.currency_id)
def isr_print(self):
""" Triggered by the 'Print ISR' button.
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from reportlab.graphics.shapes import Image as ReportLabImage
from reportlab.lib.units import mm
CH_QR_CROSS_SIZE_RATIO = 0.1214 # Ratio between the side length of the Swiss QR-code cross image and the QR-code's
CH_QR_CROSS_FILE = 'addons/l10n_ch/static/src/img/CH-Cross_7mm.png' # Image file containing the Swiss QR-code cross to add on top of the QR-code
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
@api.model
def get_available_barcode_masks(self):
rslt = super(IrActionsReport, self).get_available_barcode_masks()
rslt['ch_cross'] = self.apply_qr_code_ch_cross_mask
return rslt
@api.model
def apply_qr_code_ch_cross_mask(self, width, height, barcode_drawing):
cross_width = CH_QR_CROSS_SIZE_RATIO * width
cross_height = CH_QR_CROSS_SIZE_RATIO * height
qr_cross = ReportLabImage((width/2 - cross_width/2) / mm, (height/2 - cross_height/2) / mm, cross_width / mm, cross_height / mm, CH_QR_CROSS_FILE)
barcode_drawing.add(qr_cross)
+125 -82
View File
@@ -5,6 +5,7 @@ import re
from odoo import api, fields, models, _
from odoo.tools.misc import mod10r
from odoo.exceptions import UserError
import werkzeug.urls
@@ -98,91 +99,133 @@ class ResPartnerBank(models.Model):
return iban[-12:]
return None
def find_number(self, s):
# this regex match numbers like 1bis 1a
lmo = re.findall('([0-9]+[^ ]*)',s)
# no number found
if len(lmo) == 0:
return ''
# Only one number or starts with a number return the first one
if len(lmo) == 1 or re.match(r'^\s*([0-9]+[^ ]*)',s):
return lmo[0]
# else return the last one
if len(lmo) > 1:
return lmo[-1]
else:
return ''
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&humanreadable=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:
comment = (free_communication[:137] + '...') if len(free_communication) > 140 else free_communication
creditor_addr_1, creditor_addr_2 = self._get_partner_address_lines(self.partner_id)
debtor_addr_1, debtor_addr_2 = self._get_partner_address_lines(debtor_partner)
# Compute reference type (empty by default, only mandatory for QR-IBAN,
# and must then be 27 characters-long, with mod10r check digit as the 27th one,
# just like ISR number for invoices)
reference_type = 'NON'
reference = ''
if self._is_qr_iban():
# _check_for_qr_code_errors ensures we can't have a QR-IBAN without a QR-reference here
reference_type = 'QRR'
reference = structured_communication
currency = currency or self.currency_id or self.company_id.currency_id
return [
'SPC', # QR Type
'0200', # Version
'1', # Coding Type
self.sanitized_acc_number, # IBAN
'K', # Creditor Address Type
(self.acc_holder_name or self.partner_id.name)[:71], # Creditor Name
creditor_addr_1, # Creditor Address Line 1
creditor_addr_2, # Creditor Address Line 2
'', # Creditor Postal Code (empty, since we're using combined addres elements)
'', # Creditor Town (empty, since we're using combined addres elements)
self.partner_id.country_id.code, # Creditor Country
'', # Ultimate Creditor Address Type
'', # Name
'', # Ultimate Creditor Address Line 1
'', # Ultimate Creditor Address Line 2
'', # Ultimate Creditor Postal Code
'', # Ultimate Creditor Town
'', # Ultimate Creditor Country
'{:.2f}'.format(amount), # Amount
currency.name, # Currency
'K', # Ultimate Debtor Address Type
debtor_partner.name[:71], # Ultimate Debtor Name
debtor_addr_1, # Ultimate Debtor Address Line 1
debtor_addr_2, # Ultimate Debtor Address Line 2
'', # Ultimate Debtor Postal Code (not to be provided for address type K)
'', # Ultimate Debtor Postal City (not to be provided for address type K)
debtor_partner.country_id.code, # Ultimate Debtor Postal Country
reference_type, # Reference Type
reference, # Reference
comment, # Unstructured Message
'EPD', # Mandatory trailer part
]
def _is_qr_iban(self):
""" Tells whether or not this bank account has a QR-IBAN account number.
QR-IBANs are specific identifiers used in Switzerland as references in
QR-codes. They are formed like regular IBANs, but are actually something
different.
"""
self.ensure_one()
iid_start_index = 4
iid_end_index = 8
iid = self.sanitized_acc_number[iid_start_index : iid_end_index+1]
return self.acc_type == 'iban' \
and re.match('\d+', iid) \
and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only
@api.model
def build_swiss_code_url(self, amount, currency, date_due, debitor, ref_type, reference, comment):
communication = ""
if comment:
communication = (comment[:137] + '...') if len(comment) > 140 else comment
def _is_qr_reference(self, reference):
""" Checks whether the given reference is a QR-reference, i.e. it is
made of 27 digits, the 27th being a mod10r check on the 26 previous ones.
"""
return reference \
and len(reference) == 27 \
and re.match('\d+$', reference) \
and reference == mod10r(reference[:-1])
t_street_comp = '%s %s' % (self.company_id.street if (self.company_id.street != False) else '', self.company_id.street2 if (self.company_id.street2 != False) else '')
t_street_deb = '%s %s' % (debitor.street if (debitor.street != False) else '', debitor.street2 if (debitor.street2 != False) else '')
number = self.find_number(t_street_comp)
number_deb = self.find_number(t_street_deb)
if (t_street_comp == ' '):
t_street_comp = False
if (t_street_deb == ' '):
t_street_deb = False
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
zip and city. Those two lines are limited to 70 characters
"""
line_1 = (partner and partner.street or '') + ' ' + (partner and partner.street2 or '')
line_2 = partner.zip + ' ' + partner.city
return line_1[:71], line_2[:71]
qr_code_string = 'SPC\n0100\n1\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s' % (
self.acc_number,
self.company_id.name,
t_street_comp,
number,
self.company_id.zip,
self.company_id.city,
self.company_id.country_id.code,
amount,
currency,
date_due,
debitor.name,
t_street_deb,
number_deb,
debitor.zip,
debitor.city,
debitor.country_id.code,
ref_type,
reference,
communication)
qr_code_url = '/report/barcode/?type=%s&value=%s&width=%s&height=%s&humanreadable=1' % ('QR', werkzeug.urls.url_quote_plus(qr_code_string), 256, 256)
return qr_code_url
def _eligible_for_qr_code(self, qr_method, debtor_partner, currency):
if qr_method == 'ch_qr':
def validate_swiss_code_arguments(self, currency, debitor):
currency = currency or self.company_id.currency_id
t_street_comp = '%s %s' % (self.company_id.street if (self.company_id.street != False) else '', self.company_id.street2 if (self.company_id.street2 != False) else '')
t_street_deb = '%s %s' % (debitor.street if (debitor.street != False) else '', debitor.street2 if (debitor.street2 != False) else '')
number = self.find_number(t_street_comp)
number_deb = self.find_number(t_street_deb)
if (t_street_comp == ' '):
t_street_comp = False
if (t_street_deb == ' '):
t_street_deb = False
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')
if(currency.name == 'EUR'):
return (self.l10n_ch_isr_subscription_eur and
self.company_id.zip and
self.company_id.city and
self.company_id.country_id.code and
(t_street_comp != False) and
(t_street_deb != False) and
debitor.zip and
debitor.city and
debitor.country_id.code and
(number != False) and (number_deb != False))
elif(currency.name == 'CHF'):
return (self.l10n_ch_isr_subscription_chf and
self.company_id.zip and
self.company_id.city and
self.company_id.country_id.code and
(t_street_comp != False) and
(t_street_deb != False) and
debitor.zip and
debitor.city and
debitor.country_id.code and
(number != False) and (number_deb != False))
else:
return False
return super()._eligible_for_qr_code(qr_method, debtor_partner, currency)
def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
def _partner_fields_set(partner):
return partner.zip and \
partner.city and \
partner.country_id.code and \
(self.partner_id.street or self.partner_id.street2)
if qr_method == 'ch_qr':
if not _partner_fields_set(self.partner_id):
return _("The partner set on the bank account meant to receive the payment (%s) must have a complete postal address (street, zip, city and country)." % self.acc_number)
if debtor_partner and not _partner_fields_set(debtor_partner):
return _("The partner the QR-code is intended to must have a complete postal address (street, zip, city and country).")
if self._is_qr_iban() and not self._is_qr_reference(structured_communication):
return _("When using a QR-IBAN as the destination account of a QR-code, the payment reference must be a QR-reference.")
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
@api.model
def _get_available_qr_methods(self): #TODO OCO ajouter une séquence (pour s'assurer que toujours checké avant SEPA)
rslt = super()._get_available_qr_methods()
rslt.append(('ch_qr', _("Swiss QR bill"), 10))
return rslt
@@ -18,4 +18,3 @@ class ResConfigSettings(models.TransientModel):
related="company_id.l10n_ch_isr_scan_line_left", readonly=False)
l10n_ch_isr_scan_line_top = fields.Float(string='Vertical offset',
related="company_id.l10n_ch_isr_scan_line_top", readonly=False)
l10n_ch_print_qrcode = fields.Boolean("Print Swiss QR Code", config_parameter='l10n_ch.print_qrcode')
+6 -21
View File
@@ -2,10 +2,14 @@
<odoo>
<data>
<template id="l10n_ch_swissqr_template" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='qrcode']" position="attributes">
<attribute name="t-if">not o.display_swiss_qr_code() and o.display_qr_code</attribute>
</xpath>
<xpath expr="//div[@id='qrcode']" position="after">
<link type="text/css" rel="stylesheet" href="/l10n_ch/static/src/scss/report_swissqr.css"/>
<div class="swissqr_content" t-if="o.display_swiss_qr_code()">
<div class="swissqr_body" t-if="o.invoice_partner_bank_id.validate_swiss_code_arguments(o.invoice_partner_bank_id.currency_id, o.partner_id) and ((o.currency_id.name == 'EUR') or (o.currency_id.name == 'CHF'))">
<div class="swissqr_body" t-if="o.invoice_partner_bank_id._eligible_for_qr_code('ch_qr', o.partner_id, o.currency_id)">
<div id="title_zone" class="swissqr_column_left title_zone">
<span class="swissqr_text title title_zone">QR-bill payment part</span><br/>
</div>
@@ -15,10 +19,7 @@
<span class="swissqr_text content">Credit transfer</span>
</div>
<img class="swissqr" t-att-src="o.invoice_partner_bank_id.build_swiss_code_url(o.amount_residual, o.currency_id.name, o.invoice_date_due, o.partner_id, 'QRR',
(o.invoice_partner_bank_id.l10n_ch_isr_subscription_chf) if (o.invoice_partner_bank_id.currency_id.name == 'CHF') else o.invoice_partner_bank_id.l10n_ch_isr_subscription_eur,
(o.ref) if (o.ref) else o.name)"/>
<img class="ch_cross" src="/l10n_ch/static/src/img/CH-Cross_7mm.png"/>
<img class="swissqr" t-att-src="o.invoice_partner_bank_id.build_qr_code_url(o.amount_residual, o.ref or o.name, o.invoice_payment_ref, o.currency_id, o.partner_id, 'ch_qr')"/>
<div id="indications_zone" class="swissqr_column_right indication_zone">
<span class="swissqr_text title">Account</span><br/>
@@ -65,22 +66,6 @@
<span class="swissqr_text content" t-esc="formated_amount"/>
</div>
</div>
<div class="swissqr_body" t-if="(o.invoice_partner_bank_id.validate_swiss_code_arguments(o.invoice_partner_bank_id.currency_id, o.partner_id) == False) and ((o.currency_id.name == 'EUR') or (o.currency_id.name == 'CHF'))">
<div class="swissqr_column_left procedure_zone">
<span>Some pieces of this information are not set correctly to display the Swiss QR Code.<br/>
-Bank > CHF ISR subscription<br/>
-Bank > EUR ISR subscription<br/>
-Your company > Street with number<br/>
-Your company > City<br/>
-Your company > Zip<br/>
-Your company > Country<br/>
-Debtor > Street with number<br/>
-Debtor > City<br/>
-Debtor > Zip<br/>
-Debtor > Country
</span>
</div>
</div>
</div>
</xpath>
</template>
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_ch_qr_code
from . import test_l10n_ch_isr
from . import test_l10n_ch_swissqr
+79
View File
@@ -0,0 +1,79 @@
# -*- coding:utf-8 -*-
from odoo.exceptions import UserError
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
class TestSwissQRCode(AccountTestInvoicingCommon):
""" Tests the generation of Swiss QR-codes on invoices
"""
@classmethod
def setUpClass(cls):
super(TestSwissQRCode, cls).setUpClass()
cls.company_data['company'].qr_code = True
cls.company_data['company'].country_id = None
cls.swiss_iban = cls.env['res.partner.bank'].create({
'acc_number': 'CH15 3881 5158 3845 3843 7',
'partner_id': cls.company_data['company'].partner_id.id,
})
cls.swiss_qr_iban = cls.env['res.partner.bank'].create({
'acc_number': 'CH21 3080 8001 2345 6782 7',
'partner_id': cls.company_data['company'].partner_id.id,
})
cls.ch_qr_invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'currency_id': cls.env.ref('base.CHF').id,
'invoice_partner_bank_id': cls.swiss_iban.id,
'company_id': cls.company_data['company'].id,
'invoice_payment_ref': "Papa a vu le fifi de lolo",
'invoice_line_ids': [
(0, 0, {'quantity': 1, 'price_unit': 100})
],
})
def _assign_partner_address(self, partner):
partner.write({
'country_id': self.env.ref('base.ch').id,
'street': "Crab street, 11",
'city': "Crab City",
'zip': "4242",
})
def test_swiss_qr_code_generation(self):
""" Check different cases of Swiss QR-code generation, when qr_method is
specified beforehand.
"""
self.ch_qr_invoice.qr_code_method = 'ch_qr'
# First check with a regular IBAN
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a Swiss QR-code for partners without a complete Swiss address."):
self.ch_qr_invoice.generate_qr_code()
# Setting the address should make it work
self._assign_partner_address(self.ch_qr_invoice.company_id.partner_id)
self._assign_partner_address(self.ch_qr_invoice.partner_id)
self.ch_qr_invoice.generate_qr_code()
# Now, check with a QR-IBAN as the payment account
self.ch_qr_invoice.invoice_partner_bank_id = self.swiss_qr_iban
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a Swiss QR-cde for a QR-IBAN without giving it a valid QR-reference as payment reference."):
self.ch_qr_invoice.generate_qr_code()
# Assigning a QR reference should fix it
self.ch_qr_invoice.invoice_payment_ref = '210000000003139471430009017'
def test_ch_qr_code_detection(self):
""" Checks Swiss QR-code auto-detection when no specific QR-method
is given to the invoice.
"""
self._assign_partner_address(self.ch_qr_invoice.company_id.partner_id)
self._assign_partner_address(self.ch_qr_invoice.partner_id)
self.ch_qr_invoice.generate_qr_code()
self.assertEqual(self.ch_qr_invoice.qr_code_method, 'ch_qr', "Swiss QR-code generator should have been chosen for this invoice.")
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class SwissQRTest(TransactionCase):
def test_abc_1(self):
assert self.env['res.partner.bank'].find_number('1 rue du bois communal') == '1'
assert self.env['res.partner.bank'].find_number('1bis rue du bois communal') == '1bis'
assert self.env['res.partner.bank'].find_number('rue du bois communal 1') == '1'
assert self.env['res.partner.bank'].find_number('rue du bois communal 1bis') == '1bis'
assert self.env['res.partner.bank'].find_number('1 rue du 9 mars 1962') == '1'
assert self.env['res.partner.bank'].find_number('rue du 9 mars 1962 1') == '1'
assert self.env['res.partner.bank'].find_number('1bis rue du 9 mars 1962') == '1bis'
assert self.env['res.partner.bank'].find_number('rue du 9 mars 1962 1bis') == '1bis'
assert self.env['res.partner.bank'].find_number('rue du 9 mars 1962') == '1962'
assert self.env['res.partner.bank'].find_number('rue du 9 mars 1962 4 boite 2') == '2'
@@ -8,15 +8,6 @@
<field name="arch" type="xml">
<xpath expr="//div[@id='invoicing_settings']" position="inside">
<div class="col-12 col-lg-6 o_setting_box" id="l10n_ch-isr_print_bank" attrs="{'invisible': [('country_code', '!=', 'CH')]}">
<div class="o_setting_left_pane">
<field name="l10n_ch_print_qrcode"/>
</div>
<div class="o_setting_right_pane">
<label for="l10n_ch_print_qrcode"/>
<div class="text-muted">
Append the QR Code ISR at the end of customer invoices for Swiss customers
</div>
</div>
<div class="o_setting_left_pane">
<field name="l10n_ch_isr_print_bank_location"/>
</div>
+1 -1
View File
@@ -149,7 +149,7 @@ class PaymentAcquirer(models.Model):
fees_dom_var = fields.Float('Variable domestic fees (in percents)')
fees_int_fixed = fields.Float('Fixed international fees')
fees_int_var = fields.Float('Variable international fees (in percents)')
qr_code = fields.Boolean('Use SEPA QR Code')
qr_code = fields.Boolean('Enable QR Codes', help="Enable the use of QR-codes for payments made on this provider.")
# TDE FIXME: remove that brol
module_id = fields.Many2one('ir.module.module', string='Corresponding Module')
@@ -93,13 +93,12 @@
<span name="form_amount" class="form-control" t-esc="tx.amount" t-options="{'widget': 'monetary', 'display_currency': tx.currency_id}"/>
</div>
</div>
<div t-if="(tx.acquirer_id.qr_code == True) and (tx.currency_id.name == 'EUR')">
<div t-if="tx.acquirer_id.journal_id.bank_account_id.qr_code_valid">
<div t-if="tx.acquirer_id.qr_code">
<t t-set="qr_code" t-value="tx.acquirer_id.journal_id.bank_account_id.build_qr_code_url(tx.amount, payment_tx_id.reference, None, tx.currency_id, tx.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="tx.acquirer_id.journal_id.bank_account_id.build_qr_code_url(tx.amount,tx.reference)"/>
</div>
<div t-if="(tx.acquirer_id.journal_id.bank_account_id.qr_code_valid == False)">
<h3>The SEPA QR Code informations are not set correctly.</h3>
<img class="border border-dark rounded" t-att-src="qr_code"/>
</div>
</div>
<div>
@@ -130,13 +129,12 @@
<div t-if="payment_tx_id.acquirer_id.provider == 'transfer' and reference">
<b>Communication: </b><span t-esc='reference'/>
</div>
<div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
<div t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
<div t-if="payment_tx_id.acquirer_id.qr_code and (payment_tx_id.acquirer_id.provider == 'transfer')">
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,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="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,payment_tx_id.reference)"/>
</div>
<div t-if="(payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid == False)">
<h3>The SEPA QR Code informations are not set correctly.</h3>
<img class="border border-dark rounded" t-att-src="qr_code"/>
</div>
</div>
</div>
+5 -2
View File
@@ -1991,7 +1991,7 @@ class ReportController(http.Controller):
# Misc. route utils
#------------------------------------------------------
@http.route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="public")
def report_barcode(self, type, value, width=600, height=100, humanreadable=0, quiet=1):
def report_barcode(self, type, value, width=600, height=100, humanreadable=0, quiet=1, mask=None):
"""Contoller able to render barcode images thanks to reportlab.
Samples:
<img t-att-src="'/report/barcode/QR/%s' % o.name"/>
@@ -2005,10 +2005,13 @@ class ReportController(http.Controller):
at the bottom of the output image
:param quiet: Accepted values: 0 (default) or 1. 1 will display white
margins on left and right.
:param mask: The mask code to be used when rendering this QR-code.
Masks allow adding elements on top of the generated image,
such as the Swiss cross in the center of QR-bill codes.
"""
try:
barcode = request.env['ir.actions.report'].barcode(type, value, width=width,
height=height, humanreadable=humanreadable, quiet=quiet)
height=height, humanreadable=humanreadable, quiet=quiet, mask=mask)
except (ValueError, AttributeError):
raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.')
+5 -6
View File
@@ -1625,13 +1625,12 @@
<div t-if="payment_tx_id.acquirer_id.provider == 'transfer' and order.reference" class="card-body">
<b>Communication: </b><span t-esc='order.reference'/>
</div>
<div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.acquirer_id.provider == 'transfer') and (payment_tx_id.currency_id.name == 'EUR')">
<div class="card-body" t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
<div t-if="payment_tx_id.acquirer_id.qr_code and (payment_tx_id.acquirer_id.provider == 'transfer')">
<t t-set="qr_code" t-value="payment_tx_id.acquirer_id.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)"/>
<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="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(order.amount_total,payment_tx_id.reference)"/>
</div>
<div class="card-body" t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid == False">
<h3>The SEPA QR Code informations are not set correctly.</h3>
<img class="border border-dark rounded" t-att-src="qr_code"/>
</div>
</div>
</div>
+23 -1
View File
@@ -491,7 +491,7 @@ class IrActionsReport(models.Model):
return report_obj.with_context(context).search(conditions, limit=1)
@api.model
def barcode(self, barcode_type, value, width=600, height=100, humanreadable=0, quiet=1):
def barcode(self, barcode_type, value, width=600, height=100, humanreadable=0, quiet=1, mask=None):
if barcode_type == 'UPCA' and len(value) in (11, 12, 13):
barcode_type = 'EAN13'
if len(value) in (11, 12):
@@ -502,6 +502,15 @@ class IrActionsReport(models.Model):
barcode_type, value=value, format='png', width=width, height=height,
humanReadable=humanreadable, quiet=quiet
)
# If a mask is asked and it is available, call its function to
# post-process the generated QR-code image
if mask:
available_masks = self.get_available_barcode_masks()
mask_to_apply = available_masks.get(mask)
if mask_to_apply:
mask_to_apply(width, height, barcode)
return barcode.asString('png')
except (ValueError, AttributeError):
if barcode_type == 'Code128':
@@ -510,6 +519,19 @@ class IrActionsReport(models.Model):
return self.barcode('Code128', value, width=width, height=height,
humanreadable=humanreadable, quiet=quiet)
@api.model
def get_available_barcode_masks(self):
""" Hook for extension.
This function returns the available QR-code masks, in the form of a
list of (code, mask_function) elements, where code is a string identifying
the mask uniquely, and mask_function is a function returning a reportlab
Drawing object with the result of the mask, and taking as parameters:
- width of the QR-code, in pixels
- height of the QR-code, in pixels
- reportlab Drawing object containing the barcode to apply the mask on
"""
return {}
def render_template(self, template, values=None):
"""Allow to render a QWeb template python-side. This function returns the 'ir.ui.view'
render but embellish it with some variables/methods used in reports.
+3 -23
View File
@@ -6,6 +6,7 @@ import collections
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.exceptions import UserError
import werkzeug.urls
@@ -49,12 +50,12 @@ class Bank(models.Model):
domain = ['&'] + domain
bank_ids = self._search(domain + args, limit=limit, access_rights_uid=name_get_uid)
return models.lazy_name_get(self.browse(bank_ids).with_user(name_get_uid))
@api.onchange('country')
def _onchange_country_id(self):
if self.country and self.country != self.state.country_id:
self.state = False
@api.onchange('state')
def _onchange_state(self):
if self.state.country_id:
@@ -86,7 +87,6 @@ class ResPartnerBank(models.Model):
sequence = fields.Integer(default=10)
currency_id = fields.Many2one('res.currency', string='Currency')
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company, ondelete='cascade')
qr_code_valid = fields.Boolean(string="Has all required arguments", compute="_validate_qr_code_arguments")
_sql_constraints = [
('unique_number', 'unique(sanitized_acc_number, company_id)', 'Account Number must be unique'),
@@ -125,23 +125,3 @@ class ResPartnerBank(models.Model):
args[pos] = ('sanitized_acc_number', op, value)
pos += 1
return super(ResPartnerBank, self)._search(args, offset, limit, order, count=count, access_rights_uid=access_rights_uid)
@api.model
def build_qr_code_url(self, amount, comment):
communication = ""
if comment:
communication = (comment[:137] + '...') if len(comment) > 140 else comment
qr_code_string = 'BCD\n001\n1\nSCT\n%s\n%s\n%s\nEUR%s\n\n\n%s' % (self.bank_bic or "", self.company_id.name, self.acc_number, amount, communication)
qr_code_url = '/report/barcode/?' + werkzeug.urls.url_encode({'type': 'QR', 'value': qr_code_string, 'width': 128, 'height': 128, 'humanreadable': 1})
return qr_code_url
def _validate_qr_code_arguments(self):
sepa_zones_codes = [c.code for c in self.env.ref('base.sepa_zone').country_ids]
# Some country instances share the same IBAN country code (e.g. Åland Islands and Finland IBANs are "FI", but Åland Islands code is "AX").
# Therefore sepa_zones_codes is too permissive, "AX" is not a valid IBAN country code.
not_iban_codes = ("AX", "NC", "YT", "TF", "BL", "RE", "MF", "GP", "PM", "PF", "GF", "MQ", "JE", "GG", "IM")
sepa_zones_codes = [code for code in sepa_zones_codes if code not in not_iban_codes]
for bank in self:
bank.qr_code_valid = (bank.company_id.name and
bank.acc_number and
bank.sanitized_acc_number[:2] in sepa_zones_codes)