diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 07598a8..4c6f868 100644 --- a/README.md +++ b/README.md @@ -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** A: Make sure you have: - 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** ![QR Code Settings](static/description/qr_payment_settings_en.png) **Q: The QR code is generated on the invoice, but scanning it results in an error** 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 customer's banking app supports **SPAYD QR codes** diff --git a/__manifest__.py b/__manifest__.py index e87ce09..03e6cb3 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -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.) * **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. -* **Integration:** Works with standard Odoo invoice templates. +* **Multi-country IBAN Support:** Works with Czech (CZ), Slovak (SK), and other IBANs. +* **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). 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.', 'website': 'https://github.com/ash-technology/l10n_cz_extra', 'license': 'LGPL-3', - 'external_dependencies': { - 'python': ['qrcode'], - }, - 'data': [ - 'views/report_invoice.xml', - ], + 'data': [], 'installable': True, 'auto_install': False, 'application': False, diff --git a/models/__init__.py b/models/__init__.py index 4c89822..7ff1ea2 100755 --- a/models/__init__.py +++ b/models/__init__.py @@ -1 +1 @@ -from . import account_move \ No newline at end of file +from . import res_partner_bank diff --git a/models/account_move.py b/models/account_move.py deleted file mode 100644 index 77a64bb..0000000 --- a/models/account_move.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/models/res_partner_bank.py b/models/res_partner_bank.py new file mode 100644 index 0000000..8c4ff95 --- /dev/null +++ b/models/res_partner_bank.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 2865bc1..0316404 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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. diff --git a/views/report_invoice.xml b/views/report_invoice.xml deleted file mode 100644 index a3a9184..0000000 --- a/views/report_invoice.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - \ No newline at end of file