Rework: use native res.partner.bank qr_code_method instead of report xpath
Register 'spayd' as a proper qr_code_method on res.partner.bank (same extension point account_qr_code_sepa uses for EPC/'sct_qr'), sequence=6 so it's tried before EPC (sequence 20). Eligibility now requires CZK/EUR currency AND a Czech customer country - previously the report template replaced EPC for *any* customer as long as the invoice currency was CZK or EUR, which would incorrectly show a Czech SPAYD code to e.g. a German customer being invoiced in EUR. account.move._generate_qr_code() already tries all registered methods in priority order and the existing div#qrcode block in account.report_invoice_document already renders whatever comes back, so this needs no report template override at all. - Move get_cz_qr_code_base64()/cz_to_iban() from account_move.py into res_partner_bank.py (as _get_qr_code_generation_params/_spayd_to_iban); fix bank_code not being zero-padded to 4 digits, which could throw off the mod-97 checksum for short local bank codes. Verified the checksum against the standard 19-2000145399/0800 -> CZ6508000000192000145399 reference example and general IBAN mod-97 validity. - Remove views/report_invoice.xml (nothing left to override) and the now unnecessary qrcode Python dependency (Odoo's own barcode report action renders the image; this module only supplies the SPAYD string). - Update README FAQ to match the new eligibility rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -46,14 +46,15 @@ If you are a **Slovak company** invoicing **Czech customers in EUR**:
|
|||||||
**Q: The QR code is not generated on the invoice**
|
**Q: The QR code is not generated on the invoice**
|
||||||
A: Make sure you have:
|
A: Make sure you have:
|
||||||
- The **QR Codes** option enabled in the **Invoicing** app configuration
|
- The **QR Codes** option enabled in the **Invoicing** app configuration
|
||||||
- A bank account set up for your company with the **same currency as the invoice** (EUR or CZK)
|
- The invoice's bank account (**Payment QR-code** section) is in **CZK or EUR**
|
||||||
|
- The **customer's country is set to Czech Republic** - SPAYD is only offered
|
||||||
|
automatically for Czech customers; for anyone else Odoo falls back to EPC
|
||||||
- The invoice is **confirmed/posted**
|
- The invoice is **confirmed/posted**
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
**Q: The QR code is generated on the invoice, but scanning it results in an error**
|
**Q: The QR code is generated on the invoice, but scanning it results in an error**
|
||||||
A: Check whether:
|
A: Check whether:
|
||||||
- A bank account for the **invoice currency** (EUR or CZK) is set up for the company
|
|
||||||
- The bank account number is in **valid format** (Czech format `123-456/0800` or IBAN)
|
- The bank account number is in **valid format** (Czech format `123-456/0800` or IBAN)
|
||||||
- The customer's banking app supports **SPAYD QR codes**
|
- The customer's banking app supports **SPAYD QR codes**
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -20,8 +20,12 @@ Key Features:
|
|||||||
-------------
|
-------------
|
||||||
* **Standard SPAYD Support:** Fully compatible with all major Czech banking apps (ČSOB, Komerční banka, Česká spořitelna, etc.)
|
* **Standard SPAYD Support:** Fully compatible with all major Czech banking apps (ČSOB, Komerční banka, Česká spořitelna, etc.)
|
||||||
* **Smart IBAN Conversion:** Automatically converts local account numbers (e.g. 123-456/0800) to IBAN format.
|
* **Smart IBAN Conversion:** Automatically converts local account numbers (e.g. 123-456/0800) to IBAN format.
|
||||||
* **Multi-country IBAN Support:** Works with Czech (CZ), Slovak (SK), and other SEPA IBANs.
|
* **Multi-country IBAN Support:** Works with Czech (CZ), Slovak (SK), and other IBANs.
|
||||||
* **Integration:** Works with standard Odoo invoice templates.
|
* **Integration:** Registers as a native Odoo QR-code method (res.partner.bank
|
||||||
|
qr_code_method='spayd', same mechanism account_qr_code_sepa uses for EPC) -
|
||||||
|
no changes to the invoice print template are needed, and it's automatically
|
||||||
|
preferred over EPC for Czech customers, falling back to EPC otherwise.
|
||||||
|
Users can still switch manually via the "Payment QR-code" field.
|
||||||
* **Currency Support:** CZK and EUR (for cross-border payments).
|
* **Currency Support:** CZK and EUR (for cross-border payments).
|
||||||
|
|
||||||
Usage for Slovak Companies:
|
Usage for Slovak Companies:
|
||||||
@@ -37,12 +41,7 @@ Note: Czech banks will process EUR payments if the recipient has a EUR account.
|
|||||||
'author': 'Ash technology s.r.o.',
|
'author': 'Ash technology s.r.o.',
|
||||||
'website': 'https://github.com/ash-technology/l10n_cz_extra',
|
'website': 'https://github.com/ash-technology/l10n_cz_extra',
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
'external_dependencies': {
|
'data': [],
|
||||||
'python': ['qrcode'],
|
|
||||||
},
|
|
||||||
'data': [
|
|
||||||
'views/report_invoice.xml',
|
|
||||||
],
|
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
from . import account_move
|
from . import res_partner_bank
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import qrcode
|
|
||||||
import base64
|
|
||||||
import io
|
|
||||||
from odoo import models, api
|
|
||||||
|
|
||||||
class AccountMove(models.Model):
|
|
||||||
_inherit = 'account.move'
|
|
||||||
|
|
||||||
def get_cz_qr_code_base64(self):
|
|
||||||
""" Returns Base64 PNG image for the invoice PDF """
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
# Support both EUR and CZK currencies
|
|
||||||
# (for Slovak companies invoicing Czech customers in EUR)
|
|
||||||
allowed_currencies = ['CZK', 'EUR']
|
|
||||||
if self.currency_id.name not in allowed_currencies:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Find bank: 1. Matching currency, 2. No currency set (default)
|
|
||||||
bank = self.company_id.partner_id.bank_ids.filtered(lambda b: b.currency_id == self.currency_id)[:1]
|
|
||||||
if not bank:
|
|
||||||
bank = self.company_id.partner_id.bank_ids.filtered(lambda b: not b.currency_id)[:1]
|
|
||||||
|
|
||||||
if not bank or not bank.acc_number:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Build SPAYD string
|
|
||||||
# Note: cz_to_iban handles both CZ format (123-456/0800) and IBAN format (SK..., CZ..., etc.)
|
|
||||||
iban = self.cz_to_iban(bank.acc_number)
|
|
||||||
amount = '%.2f' % self.amount_residual
|
|
||||||
vs = ''.join(filter(str.isdigit, self.payment_reference or ''))[-10:]
|
|
||||||
currency = self.currency_id.name or 'EUR'
|
|
||||||
|
|
||||||
spayd = f"SPD*1.0*ACC:{iban}*AM:{amount}*CC:{currency}*X-VS:{vs}"
|
|
||||||
|
|
||||||
# Generate QR Image
|
|
||||||
qr = qrcode.make(spayd)
|
|
||||||
buf = io.BytesIO()
|
|
||||||
qr.save(buf, format="PNG")
|
|
||||||
return base64.b64encode(buf.getvalue()).decode('utf-8')
|
|
||||||
|
|
||||||
def cz_to_iban(self, acc_number):
|
|
||||||
"""
|
|
||||||
Converts Czech account number formats to IBAN.
|
|
||||||
|
|
||||||
Supports:
|
|
||||||
- Czech local format: '123-456/0800' or '456/0800'
|
|
||||||
- IBAN format: 'CZ6508000000001234567890', 'SK6509000000005123456789', etc.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Valid IBAN (uppercase) or original input if conversion fails
|
|
||||||
"""
|
|
||||||
if not acc_number:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Clean the input
|
|
||||||
clean_acc = acc_number.replace(" ", "").upper()
|
|
||||||
|
|
||||||
# If already an IBAN (any country), return it
|
|
||||||
# IBAN format: 2-letter country code + 2-digit checksum + account number
|
|
||||||
if len(clean_acc) >= 4 and clean_acc[:2].isalpha() and clean_acc[2:4].isdigit():
|
|
||||||
return clean_acc
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Split into [prefix-main, bank_code]
|
|
||||||
parts = clean_acc.split('/')
|
|
||||||
if len(parts) != 2:
|
|
||||||
return clean_acc # Fallback for non-CZ format
|
|
||||||
|
|
||||||
bank_code = parts[1]
|
|
||||||
account_parts = parts[0].split('-')
|
|
||||||
|
|
||||||
if len(account_parts) == 2:
|
|
||||||
prefix = account_parts[0].zfill(6)
|
|
||||||
main = account_parts[1].zfill(10)
|
|
||||||
else:
|
|
||||||
prefix = "000000"
|
|
||||||
main = account_parts[0].zfill(10)
|
|
||||||
|
|
||||||
# 1. Start with Bank Code + Prefix + Main + Country Number (CZ=12, 35=Z) + 00
|
|
||||||
# Czech Republic code is 123500 (12 for C, 35 for Z)
|
|
||||||
check_string = f"{bank_code}{prefix}{main}123500"
|
|
||||||
|
|
||||||
# 2. Calculate checksum
|
|
||||||
checksum = 98 - (int(check_string) % 97)
|
|
||||||
checksum_str = str(checksum).zfill(2)
|
|
||||||
|
|
||||||
return f"CZ{checksum_str}{bank_code}{prefix}{main}"
|
|
||||||
except Exception:
|
|
||||||
return clean_acc
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from odoo import api, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartnerBank(models.Model):
|
||||||
|
_inherit = 'res.partner.bank'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_available_qr_methods(self):
|
||||||
|
rslt = super()._get_available_qr_methods()
|
||||||
|
# Nižšia sequence ako natívny EPC/SCT QR (sequence 20 v
|
||||||
|
# account_qr_code_sepa) - SPAYD sa uprednostní, keď je eligible.
|
||||||
|
rslt.append(('spayd', _("SPAYD (Czech QR Platba)"), 6))
|
||||||
|
return rslt
|
||||||
|
|
||||||
|
def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency):
|
||||||
|
if qr_method == 'spayd':
|
||||||
|
if currency.name not in ('CZK', 'EUR'):
|
||||||
|
return _("SPAYD podporuje len menu CZK alebo EUR.")
|
||||||
|
if not self._spayd_to_iban():
|
||||||
|
return _("Bankový účet nemá platné číslo účtu ani IBAN pre SPAYD.")
|
||||||
|
if debtor_partner and debtor_partner.country_id and debtor_partner.country_id.code != 'CZ':
|
||||||
|
return _("SPAYD je určený pre zákazníkov z Českej republiky.")
|
||||||
|
return None
|
||||||
|
return super()._get_error_messages_for_qr(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 == 'spayd':
|
||||||
|
return None
|
||||||
|
return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||||
|
|
||||||
|
def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
|
||||||
|
if qr_method == 'spayd':
|
||||||
|
iban = self._spayd_to_iban()
|
||||||
|
vs_source = structured_communication or free_communication or ''
|
||||||
|
vs = ''.join(filter(str.isdigit, vs_source))[-10:]
|
||||||
|
spayd = f"SPD*1.0*ACC:{iban}*AM:{amount:.2f}*CC:{currency.name}*X-VS:{vs}"
|
||||||
|
return {
|
||||||
|
'barcode_type': 'QR',
|
||||||
|
'quiet': 0,
|
||||||
|
'width': 128,
|
||||||
|
'height': 128,
|
||||||
|
'humanreadable': 0,
|
||||||
|
'value': spayd,
|
||||||
|
}
|
||||||
|
return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
|
||||||
|
|
||||||
|
def _spayd_to_iban(self):
|
||||||
|
"""Vráti IBAN pre tento bankový účet, vhodný pre SPAYD QR (ACC pole).
|
||||||
|
|
||||||
|
Podporuje český lokálny formát ('123-456/0800' alebo '456/0800') aj
|
||||||
|
priamo zadaný IBAN (akejkoľvek krajiny).
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
acc_number = self.acc_number
|
||||||
|
if not acc_number:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
clean_acc = acc_number.replace(' ', '').upper()
|
||||||
|
|
||||||
|
# Už IBAN (ľubovoľná krajina): 2 písmená + 2-číslicový kontrolný súčet.
|
||||||
|
if len(clean_acc) >= 4 and clean_acc[:2].isalpha() and clean_acc[2:4].isdigit():
|
||||||
|
return clean_acc
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = clean_acc.split('/')
|
||||||
|
if len(parts) != 2:
|
||||||
|
return clean_acc # nie je český formát ani IBAN - vrátime pôvodné
|
||||||
|
|
||||||
|
bank_code = parts[1].zfill(4)
|
||||||
|
account_parts = parts[0].split('-')
|
||||||
|
if len(account_parts) == 2:
|
||||||
|
prefix = account_parts[0].zfill(6)
|
||||||
|
main = account_parts[1].zfill(10)
|
||||||
|
else:
|
||||||
|
prefix = '000000'
|
||||||
|
main = account_parts[0].zfill(10)
|
||||||
|
|
||||||
|
# ISO 7064 mod 97-10: BBAN + krajina ako čísla (C=12, Z=35) + '00'.
|
||||||
|
check_string = f"{bank_code}{prefix}{main}123500"
|
||||||
|
checksum = 98 - (int(check_string) % 97)
|
||||||
|
return f"CZ{str(checksum).zfill(2)}{bank_code}{prefix}{main}"
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return clean_acc
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
qrcode
|
# Žiadne - QR obrázok generuje natívny Odoo report barcode mechanizmus
|
||||||
|
# (ir.actions.report.barcode), tento modul mu len dodáva SPAYD reťazec.
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<odoo>
|
|
||||||
<template id="report_invoice_document_cz" inherit_id="account.report_invoice_document">
|
|
||||||
<xpath expr="//div[@id='qrcode']" position="replace">
|
|
||||||
<t t-set="cz_qr_base64" t-value="o.get_cz_qr_code_base64()"/>
|
|
||||||
|
|
||||||
<t t-if="cz_qr_base64">
|
|
||||||
<div id="qrcode" class="d-flex mb-3 avoid-page-break-inside">
|
|
||||||
<div class="qrcode me-3">
|
|
||||||
<img t-att-src="'data:image/png;base64,%s' % cz_qr_base64" style="width:140px;height:140px;"/>
|
|
||||||
</div>
|
|
||||||
<div class="d-inline text-muted lh-sm fst-italic">
|
|
||||||
<p class="fw-bold mb-0" style="color: black;">QR Platba</p>
|
|
||||||
<p>Scan this QR Code with<br/>your banking application</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<div id="qrcode" class="d-flex mb-3 avoid-page-break-inside" t-if="o.display_qr_code and o.amount_residual">
|
|
||||||
<div class="qrcode me-3" id="qrcode_image">
|
|
||||||
<t t-set="qr_code_url" t-value="o._generate_qr_code(silent_errors=True)"/>
|
|
||||||
<p t-if="qr_code_url" class="position-relative mb-0">
|
|
||||||
<img t-att-src="qr_code_url"/>
|
|
||||||
<img src="/account/static/src/img/Odoo_logo_O.svg" id="qrcode_odoo_logo" class="top-50 start-50 position-absolute bg-white border border-3 rounded-circle"/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="d-inline text-muted lh-sm fst-italic" id="qrcode_info" t-if="qr_code_url">
|
|
||||||
<p>Scan this QR Code with<br/>your banking application</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</xpath>
|
|
||||||
</template>
|
|
||||||
</odoo>
|
|
||||||
Reference in New Issue
Block a user