""" Fusion Accounting - Payment QR Codes Extends ``account.move`` to generate **EPC QR codes** (European Payments Council Quick Response Code) on invoices, allowing customers to scan and pay directly from their banking app. The EPC QR format is defined by the `European Payments Council — Guidelines for QR Code `_. Dependencies ------------ Requires the ``qrcode`` Python library (declared in the module manifest under ``external_dependencies``). """ import base64 import io import logging import re from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError _log = logging.getLogger(__name__) try: import qrcode from qrcode.constants import ERROR_CORRECT_M except ImportError: qrcode = None _log.warning( "The 'qrcode' Python library is not installed. " "EPC QR code generation will be unavailable." ) # IBAN validation (basic structural check) _IBAN_RE = re.compile(r'^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$') class FusionPaymentQR(models.Model): """Adds EPC QR code generation to customer invoices. The EPC QR standard encodes payment instructions into a QR code that European banking apps can read to pre-fill a SEPA Credit Transfer. EPC QR Data Format ------------------ The payload is a UTF-8 string with fields separated by newlines:: BCD # Service Tag (fixed) 002 # Version 1 # Character Set (1 = UTF-8) SCT # Identification (SEPA Credit Transfer) # BIC of the beneficiary bank # Beneficiary name (max 70 chars) # Beneficiary IBAN EUR # Amount with currency prefix # Purpose code (optional, max 4 chars) # Remittance reference (max 35 chars) # Unstructured remittance info (max 140) # Beneficiary to originator info (optional) """ _inherit = 'account.move' # ------------------------------------------------------------------ # Fields # ------------------------------------------------------------------ fusion_qr_code_image = fields.Binary( string='Payment QR Code', compute='_compute_fusion_qr_code', help="EPC QR code for this invoice. Customers can scan this " "with their banking app to initiate payment.", ) fusion_qr_code_available = fields.Boolean( string='QR Code Available', compute='_compute_fusion_qr_code', help="Indicates whether a QR code can be generated for this " "invoice (depends on IBAN / BIC configuration).", ) # ------------------------------------------------------------------ # Computed fields # ------------------------------------------------------------------ @api.depends( 'state', 'move_type', 'amount_residual', 'company_id', 'partner_id', 'currency_id', ) def _compute_fusion_qr_code(self): """Compute the QR code image for eligible invoices.""" for move in self: if ( move.move_type in ('out_invoice', 'out_refund') and move.state == 'posted' and move.amount_residual > 0 and qrcode is not None ): try: qr_bytes = move.generate_epc_qr() move.fusion_qr_code_image = base64.b64encode(qr_bytes) move.fusion_qr_code_available = True except (UserError, ValidationError): move.fusion_qr_code_image = False move.fusion_qr_code_available = False else: move.fusion_qr_code_image = False move.fusion_qr_code_available = False # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ def generate_epc_qr(self): """Generate an EPC QR code image for this invoice. The QR code encodes payment instructions following the European Payments Council standard so that banking apps can pre-fill a SEPA Credit Transfer form. :return: PNG image bytes of the QR code. :rtype: bytes :raises UserError: if the ``qrcode`` library is not installed or required bank details are missing. """ self.ensure_one() if qrcode is None: raise UserError(_( "The 'qrcode' Python library is required to generate " "payment QR codes. Install it with: pip install qrcode[pil]" )) # Gather bank details company = self.company_id company_bank = company.partner_id.bank_ids[:1] if not company_bank: raise UserError(_( "No bank account is configured for company '%(company)s'. " "Please add a bank account with an IBAN in the company " "settings.", company=company.name, )) iban = (company_bank.acc_number or '').upper().replace(' ', '') if not _IBAN_RE.match(iban): raise UserError(_( "The company bank account '%(acc)s' does not appear to " "be a valid IBAN.", acc=company_bank.acc_number, )) bic = (company_bank.bank_bic or '').upper().replace(' ', '') # Amount and currency if self.currency_id.name != 'EUR': raise UserError(_( "EPC QR codes are only supported for invoices in EUR. " "This invoice uses %(currency)s.", currency=self.currency_id.name, )) amount = self.amount_residual if amount <= 0 or amount > 999999999.99: raise UserError(_( "Amount must be between 0.01 and 999,999,999.99 for " "EPC QR codes." )) # Build EPC QR payload beneficiary_name = (company.name or '')[:70] reference = (self.payment_reference or self.name or '')[:35] unstructured = '' if not reference: unstructured = ( f'Invoice {self.name}' if self.name else '' )[:140] # EPC QR payload lines (Version 002) lines = [ 'BCD', # Service Tag '002', # Version '1', # Character set (UTF-8) 'SCT', # Identification code bic, # BIC (may be empty) beneficiary_name, # Beneficiary name iban, # Beneficiary IBAN f'EUR{amount:.2f}', # Amount '', # Purpose (optional) reference if reference else '', # Structured reference unstructured if not reference else '', # Unstructured ref '', # Beneficiary info (optional) ] payload = '\n'.join(lines) # Validate payload size (EPC QR max 331 bytes) if len(payload.encode('utf-8')) > 331: raise UserError(_( "The QR code payload exceeds the EPC maximum of 331 " "bytes. Please shorten the payment reference." )) # Generate QR code image qr = qrcode.QRCode( version=None, # auto-size error_correction=ERROR_CORRECT_M, box_size=10, border=4, ) qr.add_data(payload) qr.make(fit=True) img = qr.make_image(fill_color='black', back_color='white') # Export to PNG bytes buffer = io.BytesIO() img.save(buffer, format='PNG') return buffer.getvalue()