import socket import logging import threading from odoo import models, fields, api, _ _logger = logging.getLogger(__name__) 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 from POS frontend and sends ESC/POS string to network printer. Optimized to use background threading to prevent blocking the POS transaction/RPC. """ printer = self.browse(printer_id) if not printer or not printer.network_printer_ip: return {'successful': False, 'message': 'Printer IP not configured'} # Start printing in a background thread to avoid POS timeout # We don't need a new cursor here as we've already browsed the printer # and we don't write to the database in the thread. thread = threading.Thread(target=self._threaded_print, args=(printer.network_printer_ip, receipt_data)) thread.daemon = True # Ensure thread doesn't block server shutdown thread.start() return {'successful': True, 'message': 'Print job sent to background queue'} def _threaded_print(self, ip, receipt_data): """Perform the actual socket communication in the background.""" try: # Build basic ESC/POS commands 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_DOUBLE + 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 # Send to printer s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5.0) # Slightly longer timeout for background print s.connect((ip, 9100)) s.sendall(data) s.close() _logger.info("Successfully printed to kitchen printer at %s", ip) except Exception as e: _logger.error("Background kitchen print failed for IP %s: %s", ip, e)