-
Payable to
-
-
-
-
-
-
-
-
- Reference
-
-
-
-
Additional information
-
-
-
-
-
Payable by
-
-
-
-
-
-
-
-
Due date
-
-
-
-
Currency
-
+
+ Account / Payable to
+
+
+
+
+
+
-
-
Amount
-
+
+
+ Reference
+
+
+
+
+
+
+
+ Additional information
+
+
+
+
+
+ Payable by
+
+
+
+
+
+
+
+
+
+
+
+
+ Currency
+
+
+
+ Amount
+
+
@@ -124,9 +151,9 @@
-
-
-
+
+
+
diff --git a/addons/l10n_ch/static/src/scss/report_swissqr.scss b/addons/l10n_ch/static/src/scss/report_swissqr.scss
index 379ac0690a4..2eefad0eba0 100644
--- a/addons/l10n_ch/static/src/scss/report_swissqr.scss
+++ b/addons/l10n_ch/static/src/scss/report_swissqr.scss
@@ -19,13 +19,13 @@ body.l10n_ch_qr {
.swissqr_receipt {
position: absolute;
- background_color: white;
+ background-color: white;
border-color:black;
border-width: 1pt 1pt 1pt 1pt;
border-style: solid;
height: 131mm; /* 105 * 1.25 - 0.25 mm to cope for rendering issues due to header (probably a rounding issue) */
- width: 77.5mm; /* 62 * 1.25 */
- top: 240mm; /* 192 * 1.25 */
+ width: 77.5mm; /* 62 * 1.25 */
+ top: 240mm; /* 192 * 1.25 */
left: 0mm;
/*These measures are multiplied by 1.25 here to fit the canvas size used by our reporting engine (1.25 times bigger than A4)*/
}
@@ -37,56 +37,63 @@ body.l10n_ch_qr {
border-width: 1pt 1pt 1pt 1pt;
border-style: solid;
height: 131mm; /* 105 * 1.25 - 0.25mm to cope for rendering issues due to header (probably a rounding issue)*/
- width: 185mm; /* 148 * 1.25 */
- top: 240mm; /* 192 * 1.25 */
- left: 77.5mm; /* 62 * 1.25 */
+ width: 185mm; /* 148 * 1.25 */
+ top: 240mm; /* 192 * 1.25 */
+ left: 77.5mm; /* 62 * 1.25 */
/*These measures are multiplied by 1.25 here to fit the canvas size used by our reporting engine (1.25 times bigger than A4)*/
+
}
.swissqr {
position: absolute;
- height: 70mm;
- width: 70mm;
- top: 29.5mm;
- left: 7mm;
+ height: 57.5mm; /* 46 * 1.25 */
+ width: 57.5mm; /* 46 * 1.25 */
+ top: 20mm; /* 16 * 1.25 */
+ left: 6.25mm; /* 5 * 1.25 min readability margin for QR */
+ /*These measures are multiplied by 1.25 here to fit the canvas size used by our reporting engine (1.25 times bigger than A4)*/
}
.ch_cross {
position: absolute;
background-color: white;
- height: 8.5mm;
- width: 8.5mm;
- top: 60.25mm;
- left: 37.75mm;
+ height: 8.75mm; /* 7 * 1.25 */
+ width: 8.75mm; /* 7 * 1.25 */
+ top: 44.375mm; /* (16 + 23 - 7/2) * 1.25 = 41.5 * 1.25 */
+ left: 30.625mm; /* (5 + 23 - 7/2) * 1.25 = 24.5 * 1.25 */
+ /*These measures are multiplied by 1.25 here to fit the canvas size used by our reporting engine (1.25 times bigger than A4)*/
}
.swissqr_text {
- font-family: OCR-B, Arial, Frutiger, Helvetica;
+ font-family: Arial, Frutiger, Helvetica;
color: black;
+ line-height: 1;
+ padding-top: 1em;
+
+ .title {
+ font-size: 8pt;
+ font-weight: bold;
+ margin-top: 2mm;
+ }
+ .content {
+ font-size: 10pt;
+ }
+
}
- .swissqr_text.title {
- font-size: 9pt;
- font-weight: bold;
- }
-
- .swissqr_text.title.title_zone {
+ .main_title {
+ margin-top: 5mm;
font-size: 11pt;
font-weight: bold;
}
- .swissqr_text.content {
- font-size: 11pt;
- }
-
.swissqr_column_left {
position: absolute;
- left: 12mm;
+ left: 5mm;
}
.swissqr_column_right {
position: absolute;
- left: 83mm;
+ left: 70mm; /* (5 + 46 + 5) * 1.25 */
max-width: 90mm;
}
@@ -94,23 +101,45 @@ body.l10n_ch_qr {
top: 15mm;
}
- .title_zone {
- top: 5mm;
+ .receipt_indication_zone {
+ top: 10mm;
}
.indication_zone {
top: 10mm;
}
- .amount_zone {
+ .receipt_amount_zone {
position: absolute;
- top: 106mm;
+ top: 86mm;
+
+ .column {
+ margin-right: 5mm;
+ float: left;
+ }
}
- .amount {
- position: absolute;
- left: 41mm;
+ .receipt_acceptance_point_zone {
+ position: relative;
+ top: 100mm;
+
+ .content {
+ float: right;
+ padding-right: 1em;
+ }
+
}
+
+ .amount_zone {
+ position: absolute;
+ top: 86mm;
+
+ .column {
+ margin-right: 5mm;
+ float: left;
+ }
+ }
+
}
diff --git a/addons/l10n_ch/tests/__init__.py b/addons/l10n_ch/tests/__init__.py
index f55248130f0..26725066f62 100644
--- a/addons/l10n_ch/tests/__init__.py
+++ b/addons/l10n_ch/tests/__init__.py
@@ -3,3 +3,4 @@
from . import test_ch_qr_code
from . import test_l10n_ch_isr
+from . import test_swissqr
diff --git a/addons/l10n_ch/tests/test_swissqr.py b/addons/l10n_ch/tests/test_swissqr.py
new file mode 100644
index 00000000000..5df778a950c
--- /dev/null
+++ b/addons/l10n_ch/tests/test_swissqr.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import time
+
+from odoo.addons.account.tests.account_test_classes import AccountingTestCase
+from odoo.exceptions import ValidationError
+from odoo.tests import tagged
+
+CH_IBAN = 'CH15 3881 5158 3845 3843 7'
+QR_IBAN = 'CH21 3080 8001 2345 6782 7'
+
+
+@tagged('post_install', '-at_install')
+class TestSwissQR(AccountingTestCase):
+
+ def setUp(self):
+ super(TestSwissQR, self).setUp()
+ # Activate SwissQR in Swiss invoices
+ self.env['ir.config_parameter'].create(
+ {'key': 'l10n_ch.print_qrcode', 'value': '1'}
+ )
+ self.customer = self.env['res.partner'].create(
+ {
+ "name": "Partner",
+ "street": "Route de Berne 41",
+ "street2": "",
+ "zip": "1000",
+ "city": "Lausanne",
+ "country_id": self.env.ref("base.ch").id,
+ }
+ )
+ self.env.user.company_id.partner_id.write(
+ {
+ "street": "Route de Berne 88",
+ "street2": "",
+ "zip": "2000",
+ "city": "Neuchâtel",
+ "country_id": self.env.ref('base.ch').id,
+ }
+ )
+ self.invoice1 = self.create_invoice('base.CHF')
+ sale_journal = self.env['account.journal'].search([("type", "=", "sale")])
+ sale_journal.invoice_reference_model = "ch"
+
+ def create_invoice(self, currency_to_use='base.CHF'):
+ """ Generates a test invoice """
+
+ product = self.env.ref("product.product_product_4")
+ acc_type = self.env.ref('account.data_account_type_current_assets')
+ account = self.env['account.account'].search(
+ [('user_type_id', '=', acc_type.id)], limit=1
+ )
+ invoice = (
+ self.env['account.move']
+ .with_context(default_type='out_invoice')
+ .create(
+ {
+ 'type': 'out_invoice',
+ 'partner_id': self.customer.id,
+ 'currency_id': self.env.ref(currency_to_use).id,
+ 'date': time.strftime('%Y') + '-12-22',
+ 'invoice_line_ids': [
+ (
+ 0,
+ 0,
+ {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'account_id': account.id,
+ 'quantity': 1,
+ 'price_unit': 42.0,
+ },
+ )
+ ],
+ }
+ )
+ )
+
+ return invoice
+
+ def create_account(self, number):
+ """ Generates a test res.partner.bank. """
+ return self.env['res.partner.bank'].create(
+ {
+ 'acc_number': number,
+ 'partner_id': self.env.user.company_id.partner_id.id,
+ }
+ )
+
+ def swissqr_not_generated(self, invoice):
+ """ Prints the given invoice and tests that no Swiss QR generation is triggered. """
+ self.assertFalse(
+ invoice.can_generate_qr_bill(),
+ 'No Swiss QR should be generated for this invoice',
+ )
+
+ def swissqr_generated(self, invoice, ref_type='NON'):
+ """ Prints the given invoice and tests that a Swiss QR generation is triggered. """
+ self.assertTrue(
+ invoice.can_generate_qr_bill(), 'A Swiss QR can be generated'
+ )
+
+ if ref_type == 'QRR':
+ self.assertTrue(invoice.invoice_payment_ref)
+ struct_ref = invoice.invoice_payment_ref
+ unstr_msg = invoice.ref or invoice.name or ''
+ else:
+ struct_ref = ''
+ unstr_msg = invoice.invoice_payment_ref or invoice.ref or invoice.name or ''
+ unstr_msg = (unstr_msg or invoice.number).replace('/', '%2F')
+
+ payload = (
+ "SPC%0A"
+ "0200%0A"
+ "1%0A"
+ "{iban}%0A"
+ "K%0A"
+ "YourCompany%0A"
+ "Route+de+Berne+88%0A"
+ "2000+Neuch%C3%A2tel%0A"
+ "%0A%0A"
+ "CH%0A"
+ "%0A%0A%0A%0A%0A%0A%0A"
+ "42.00%0A"
+ "CHF%0A"
+ "K%0A"
+ "Partner%0A"
+ "Route+de+Berne+41%0A"
+ "1000+Lausanne%0A"
+ "%0A%0A"
+ "CH%0A"
+ "{ref_type}%0A"
+ "{struct_ref}%0A"
+ "{unstr_msg}%0A"
+ "EPD"
+ ).format(
+ iban=invoice.partner_bank_id.sanitized_acc_number,
+ ref_type=ref_type,
+ struct_ref=struct_ref or '',
+ unstr_msg=unstr_msg,
+ )
+
+ expected_url = ("/report/barcode/?type=QR&value={}"
+ "&width=256&height=256&quiet=1").format(payload)
+
+ url = invoice.partner_bank_id.build_swiss_code_url(
+ invoice.amount_residual,
+ invoice.currency_id.name,
+ None,
+ invoice.partner_id,
+ None,
+ invoice.invoice_payment_ref,
+ invoice.ref or invoice.name,
+ )
+ self.assertEqual(url, expected_url)
+
+ def test_swissQR_missing_bank(self):
+ # Let us test the generation of a SwissQR for an invoice, first by showing an
+ # QR is included in the invoice is only generated when Odoo has all the data it needs.
+ self.invoice1.post()
+ self.swissqr_not_generated(self.invoice1)
+
+ def test_swissQR_iban(self):
+ # Now we add an account for payment to our invoice
+ # Here we don't use a structured reference
+ iban_account = self.create_account(CH_IBAN)
+ self.invoice1.partner_bank_id = iban_account
+ self.invoice1.post()
+ self.swissqr_generated(self.invoice1, ref_type="NON")
+
+ def test_swissQR_qriban(self):
+ # Now use a proper QR-IBAN, we are good to print a QR Bill
+ qriban_account = self.create_account(QR_IBAN)
+ self.assertTrue(qriban_account.acc_type, 'qr-iban')
+ self.invoice1.partner_bank_id = qriban_account
+ self.invoice1.post()
+ self.swissqr_generated(self.invoice1, ref_type="QRR")
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index fd4cca16947..0a927ccacc0 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -499,9 +499,15 @@ class IrActionsReport(models.Model):
value = '0%s' % value
try:
width, height, humanreadable, quiet = int(width), int(height), bool(int(humanreadable)), bool(int(quiet))
+ # for `QR` type, `quiet` is not supported. And is simply ignored.
+ # But we can use `barBorder` to get a similar behaviour.
+ bar_border = 4
+ if barcode_type == 'QR' and quiet:
+ bar_border = 0
+
barcode = createBarcodeDrawing(
barcode_type, value=value, format='png', width=width, height=height,
- humanReadable=humanreadable, quiet=quiet
+ humanReadable=humanreadable, quiet=quiet, barBorder=bar_border
)
# If a mask is asked and it is available, call its function to