diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py
index 7ef7d6a36d6..7aeedf8b24d 100644
--- a/addons/account/models/__init__.py
+++ b/addons/account/models/__init__.py
@@ -23,3 +23,4 @@ from . import digest
from . import res_users
from . import ir_actions_report
from . import res_currency
+from . import res_bank
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index a42c7d39928..b4010d860a8 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -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"
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 58b479428cf..43cc692d7d3 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -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 = '''
+
+
+
+ {txt}
+ '''.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.
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index c7289b0e2c2..3791f20aa0c 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -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)
diff --git a/addons/account/models/res_bank.py b/addons/account/models/res_bank.py
new file mode 100644
index 00000000000..5b587866eef
--- /dev/null
+++ b/addons/account/models/res_bank.py
@@ -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
\ No newline at end of file
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index 3ea53a26ea5..d6bb95c2a32 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -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)
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index 30df05d623d..d52ea97175f 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -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
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml
index 3575d08e5d9..5778e008d34 100644
--- a/addons/account/views/account_move_views.xml
+++ b/addons/account/views/account_move_views.xml
@@ -571,6 +571,7 @@
+
@@ -1001,6 +1002,7 @@
context="{'default_partner_id': commercial_partner_id}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
+
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index 4115310185f..60765496d67 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -128,6 +128,8 @@
+
+
@@ -202,6 +204,13 @@
+
+
+
+
+ title="Add a QR-code to your invoices so that your customers can pay instantly with their mobile banking application.">
-
+
-
+
- Add a payment QR code to your invoices
+ Add a payment QR-code to your invoices
diff --git a/addons/account_qr_code_sepa/__init__.py b/addons/account_qr_code_sepa/__init__.py
new file mode 100644
index 00000000000..8d60a0f8187
--- /dev/null
+++ b/addons/account_qr_code_sepa/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import tests
\ No newline at end of file
diff --git a/addons/account_qr_code_sepa/__manifest__.py b/addons/account_qr_code_sepa/__manifest__.py
new file mode 100644
index 00000000000..0c3438485e4
--- /dev/null
+++ b/addons/account_qr_code_sepa/__manifest__.py
@@ -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,
+}
\ No newline at end of file
diff --git a/addons/account_qr_code_sepa/models/__init__.py b/addons/account_qr_code_sepa/models/__init__.py
new file mode 100644
index 00000000000..6570df4c3b8
--- /dev/null
+++ b/addons/account_qr_code_sepa/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import res_bank
\ No newline at end of file
diff --git a/addons/account_qr_code_sepa/models/res_bank.py b/addons/account_qr_code_sepa/models/res_bank.py
new file mode 100644
index 00000000000..afd47553cc0
--- /dev/null
+++ b/addons/account_qr_code_sepa/models/res_bank.py
@@ -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
diff --git a/addons/account_qr_code_sepa/tests/__init__.py b/addons/account_qr_code_sepa/tests/__init__.py
new file mode 100644
index 00000000000..b570597d7f4
--- /dev/null
+++ b/addons/account_qr_code_sepa/tests/__init__.py
@@ -0,0 +1,3 @@
+# *-* coding:utf-8 *-*
+
+from . import test_sepa_qr
\ No newline at end of file
diff --git a/addons/account_qr_code_sepa/tests/test_sepa_qr.py b/addons/account_qr_code_sepa/tests/test_sepa_qr.py
new file mode 100644
index 00000000000..27575632bc3
--- /dev/null
+++ b/addons/account_qr_code_sepa/tests/test_sepa_qr.py
@@ -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.")
+
diff --git a/addons/l10n_ch/models/__init__.py b/addons/l10n_ch/models/__init__.py
index a5aa475652d..b4ae72a5547 100644
--- a/addons/l10n_ch/models/__init__.py
+++ b/addons/l10n_ch/models/__init__.py
@@ -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
\ No newline at end of file
+from . import account_bank_statement
+from . import ir_actions_report
diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py
index 74a20da4465..fba400be9db 100644
--- a/addons/l10n_ch/models/account_invoice.py
+++ b/addons/l10n_ch/models/account_invoice.py
@@ -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.
diff --git a/addons/l10n_ch/models/ir_actions_report.py b/addons/l10n_ch/models/ir_actions_report.py
new file mode 100644
index 00000000000..c77137c8ea6
--- /dev/null
+++ b/addons/l10n_ch/models/ir_actions_report.py
@@ -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)
\ No newline at end of file
diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py
index 5feb7dd6676..2cf6709e67d 100644
--- a/addons/l10n_ch/models/res_bank.py
+++ b/addons/l10n_ch/models/res_bank.py
@@ -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
diff --git a/addons/l10n_ch/models/res_config_settings.py b/addons/l10n_ch/models/res_config_settings.py
index e8415164d88..0b972855e66 100644
--- a/addons/l10n_ch/models/res_config_settings.py
+++ b/addons/l10n_ch/models/res_config_settings.py
@@ -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')
diff --git a/addons/l10n_ch/report/swissqr_report.xml b/addons/l10n_ch/report/swissqr_report.xml
index 261e1bc356f..795c675bbf9 100644
--- a/addons/l10n_ch/report/swissqr_report.xml
+++ b/addons/l10n_ch/report/swissqr_report.xml
@@ -2,10 +2,14 @@
+
+ not o.display_swiss_qr_code() and o.display_qr_code
+
+
-
+
QR-bill payment part
@@ -15,10 +19,7 @@
Credit transfer
-
-
+
Account
@@ -65,22 +66,6 @@
-
-
- Some pieces of this information are not set correctly to display the Swiss QR Code.
- -Bank > CHF ISR subscription
- -Bank > EUR ISR subscription
- -Your company > Street with number
- -Your company > City
- -Your company > Zip
- -Your company > Country
- -Debtor > Street with number
- -Debtor > City
- -Debtor > Zip
- -Debtor > Country
-
-
-
diff --git a/addons/l10n_ch/tests/__init__.py b/addons/l10n_ch/tests/__init__.py
index 041c87e8043..f55248130f0 100644
--- a/addons/l10n_ch/tests/__init__.py
+++ b/addons/l10n_ch/tests/__init__.py
@@ -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
diff --git a/addons/l10n_ch/tests/test_ch_qr_code.py b/addons/l10n_ch/tests/test_ch_qr_code.py
new file mode 100644
index 00000000000..e46a3a2c1ec
--- /dev/null
+++ b/addons/l10n_ch/tests/test_ch_qr_code.py
@@ -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.")
diff --git a/addons/l10n_ch/tests/test_l10n_ch_swissqr.py b/addons/l10n_ch/tests/test_l10n_ch_swissqr.py
deleted file mode 100644
index 68e91ca6466..00000000000
--- a/addons/l10n_ch/tests/test_l10n_ch_swissqr.py
+++ /dev/null
@@ -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'
diff --git a/addons/l10n_ch/views/res_config_settings_views.xml b/addons/l10n_ch/views/res_config_settings_views.xml
index 20eae297c4e..6e9d583a89f 100644
--- a/addons/l10n_ch/views/res_config_settings_views.xml
+++ b/addons/l10n_ch/views/res_config_settings_views.xml
@@ -8,15 +8,6 @@
-
-
-
-
-
-
- Append the QR Code ISR at the end of customer invoices for Swiss customers
-
-
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index afe2a1fe65b..c8c79d0e51d 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -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')
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index f21b6fa079b..49697f429a3 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -93,13 +93,12 @@
-
-
+
+
+
+
Or scan me with your banking app.
-
-
-
-
The SEPA QR Code informations are not set correctly.
+
@@ -130,13 +129,12 @@
Communication:
-
-
+
+
+
+
Or scan me with your banking app.
-
-
-
-
The SEPA QR Code informations are not set correctly.
+
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 3b0e2e27f28..b450663cd46 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1991,7 +1991,7 @@ class ReportController(http.Controller):
# Misc. route utils
#------------------------------------------------------
@http.route(['/report/barcode', '/report/barcode//'], 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:
@@ -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.')
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 230471ba92c..b0e08f346be 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1625,13 +1625,12 @@
Communication:
-
-
+
+
+
+
Or scan me with your banking app.
-
-
-
-
The SEPA QR Code informations are not set correctly.
+
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index 18b64b3c1c1..283d572de7e 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -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.
diff --git a/odoo/addons/base/models/res_bank.py b/odoo/addons/base/models/res_bank.py
index be7b8245ca8..edd0c68f68a 100644
--- a/odoo/addons/base/models/res_bank.py
+++ b/odoo/addons/base/models/res_bank.py
@@ -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)