import socket import logging import threading from odoo import models, fields, api, _ _logger = logging.getLogger(__name__) def _isolated_printer_thread(ip, data): """ Completely isolated printer thread. Does not touch Odoo environment to avoid GIL/Locking issues. """ max_retries = 3 timeout = 10.0 for attempt in range(max_retries): s = None try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Increased timeout to 10s to handle slow networks or waking printers s.settimeout(timeout) s.connect((ip, 9100)) s.sendall(data) _logger.info("POS_PRINTER: Successfully printed to %s (Attempt %d)", ip, attempt + 1) return except Exception as e: if attempt < max_retries - 1: _logger.warning("POS_PRINTER: Print attempt %d failed for IP %s: %s. Retrying...", attempt + 1, ip, e) else: _logger.error("POS_PRINTER: Background print failed for IP %s after %d attempts: %s", ip, max_retries, e) finally: if s: try: s.close() except: pass class PosPrinter(models.Model): _inherit = 'pos.printer' printer_type = fields.Selection( selection_add=[('network_escpos', 'Network ESC/POS Printer')], ondelete={'network_escpos': 'set default'} ) network_printer_ip = fields.Char( string='Network Printer IP Address', help="IP address of the generic ESC/POS network printer.", default="0.0.0.0" ) @api.model def _load_pos_data_fields(self, config_id): fields = super()._load_pos_data_fields(config_id) if 'network_printer_ip' not in fields: fields.append('network_printer_ip') return fields @api.model def print_network_kitchen_receipt(self, printer_id, receipt_data): """ Receives structural receipt_data and sends to background worker. """ printer = self.browse(printer_id) if not printer or not printer.network_printer_ip: return {'successful': False, 'message': 'Printer IP not configured'} # Build the ESC/POS data in the main thread (fast) try: ESC = b'\x1b' GS = b'\x1d' INIT = ESC + b'@' CUT = GS + b'V\x41\x00' BOLD_ON = ESC + b'E\x01' BOLD_OFF = ESC + b'E\x00' SIZE_DOUBLE = GS + b'!\x11' SIZE_NORMAL = GS + b'!\x00' ALIGN_CENTER = ESC + b'a\x01' ALIGN_LEFT = ESC + b'a\x00' NEWLINE = b'\n' data = INIT data += ALIGN_CENTER + SIZE_NORMAL + BOLD_ON data += b"KITCHEN RECEIPT" + NEWLINE data += SIZE_NORMAL + BOLD_OFF + NEWLINE if receipt_data.get('table'): data += ALIGN_LEFT + SIZE_DOUBLE data += f"Table: {receipt_data['table']}".encode('utf-8') + NEWLINE data += SIZE_NORMAL data += ALIGN_LEFT data += f"Order: {receipt_data.get('order_name', '')}".encode('utf-8') + NEWLINE data += f"Date: {receipt_data.get('order_time', '')}".encode('utf-8') + NEWLINE if receipt_data.get('waiter'): data += f"Waiter: {receipt_data['waiter']}".encode('utf-8') + NEWLINE data += b"-" * 32 + NEWLINE for line in receipt_data.get('lines', []): qty = line.get('qty', 1) name = line.get('name', '') note = line.get('note', '') data += SIZE_DOUBLE data += f"{qty} x {name}".encode('utf-8') + NEWLINE data += SIZE_NORMAL if note: data += f" NOTE: {note}".encode('utf-8') + NEWLINE data += NEWLINE data += b"-" * 32 + NEWLINE + NEWLINE + NEWLINE + CUT # Start thread t = threading.Thread(target=_isolated_printer_thread, args=(printer.network_printer_ip, data)) t.daemon = True t.start() return {'successful': True, 'message': 'Print job sent to background queue'} except Exception as e: _logger.error("POS_PRINTER: Failed to build print data: %s", e) return {'successful': False, 'message': str(e)}