From 9d874486ae2d8748a5612a7cf50bfeb37a6cf98f Mon Sep 17 00:00:00 2001 From: aserrador Date: Tue, 21 Oct 2025 14:47:37 -0500 Subject: [PATCH] fix: Refactor Tickets Formats --- Api/formats.py | 425 +++++++++++++++++++++++++++++-------------------- 1 file changed, 248 insertions(+), 177 deletions(-) diff --git a/Api/formats.py b/Api/formats.py index 0f7284e..f1d381e 100644 --- a/Api/formats.py +++ b/Api/formats.py @@ -1,196 +1,267 @@ #!/usr/bin/env python3 import sys import tempfile -import pytz - from datetime import datetime +from typing import Dict, Any, Optional + +import pytz from qr_generator import QRCodeGenerator from printer_factory import PrinterFactory -def get_current_time_america_bogota(): - format_ = "%Y-%m-%d %H:%M:%S" - america_bogota_tz = pytz.timezone('America/Bogota') - format_date_time = datetime.now(america_bogota_tz) +class TicketPrinter: + """Clase principal para manejar la impresión de tickets""" - return format_date_time.strftime(format_) + def __init__(self, address: Optional[str] = None): + self.address = address + self.printer = self._get_printer() - -def print_bill_format(printer, d, waiter): - printer.set(align='center', bold=False, height=1, width=1) - printer.text(d["shop_name"]+'\n') - printer.text(d["shop_nit"]+'\n') - printer.text(d["shop_address"]+'\n') - printer.set(align='left', bold=False, height=1, width=1) - printer.textln('===============================================') - text = d['state'] - printer.textln(text) - if d['invoice'] and d['invoice']['resolution']: - text = "Resolucion de Facturacion # " + \ - str(d['invoice']['resolution']['resolution_number']) \ - + "\nValida desde " + \ - d['invoice']['resolution']['valid_date_time_from'] + \ - " hasta "+str(d['invoice']['resolution']['valid_date_time_to']) - printer.textln(text) - printer.ln() - text = "Factura #: " + d['invoice']['invoice_number'] - printer.textln(text) - printer.text("Cliente: " + d["party"]+'\n') - printer.text("CC/NIT: " + d["tax_identifier_code"]+'\n') - printer.text("Direccion: " + d["address"]+'\n') - text = 'MESA: ' + str(d['table'] + "\n") - printer.text(text) - printer.textln('===============================================') - printer.ln() - for line in d["lines"]: - if line['type'] != 'title': - text = line['product'] - printer.text(text) - printer.ln() - text = str(line['quantity']) + " " + " $" + \ - line["unit_price"] + "\n" - printer.text(text) - - printer.set(align='right', bold=False, height=1, width=1) - printer.textln('================================================') - text = "Descuento Realizado: "+str(d["total_discount"])+"\n" - printer.text(text) - text = "Propina: "+str(d["total_tip"])+"\n" - printer.text(text) - text = "Total (sin impuestos): "+str(d["untaxed_amount"])+"\n" - printer.text(text) - text = "Impuestos (INC): "+str(d["tax_amount"])+"\n" - printer.text(text) - text = "Total: "+str(d["total"])+"\n" - printer.text(text) - printer.ln() - if 'payments' in d.keys(): - printer.textln("Pagos: ") - printer.textln('================================================') - - for payment in d['payments']: - text = str(payment["statement"])+" $"+str(payment["amount"]) - printer.textln(text) - printer.set(align='center', bold=False, height=1, width=1) - printer.textln('==============================================\n') - if d["fe_cufe"]: - QR = QRCodeGenerator(d["fe_cufe"]).generate_qr() - with tempfile.NamedTemporaryFile( - delete=True, suffix='.png', dir='/tmp') as temp_file: - temp_filename = temp_file.name - QR.save(temp_filename) - printer.image(f"{temp_filename}") - printer.set(align='left', bold=False, height=1, width=1) - text = str("CUFE: " + d['cufe']) - printer.textln(text) - printer.set(align='center', bold=False, height=1, width=1) - printer.text("Sigue nuestras redes sociales\n") - printer.text("@bicipizza\n") - printer.text("Recuerde que la propina es voluntaria.\n") - printer.text("Gracias por visitarnos, vuelva pronto.\n") - printer.text("SOFTWARE POTENCIADO POR ONECLUSTER.ORG.\n") - printer.text(str(get_current_time_america_bogota())+'\n') - if waiter: - printer.text("Atendido Por: \n") - printer.text(str(waiter)+'\n') - - -def print_customer_order_format(printer, d, waiter): - printer.set(align='center', bold=False, height=1, width=1) - printer.text(str(get_current_time_america_bogota())+'\n') - - if waiter: - printer.text("Pedido Por: \n") - printer.text(str(waiter)+'\n') - printer.set( - align='center', bold=False, height=2, width=2, custom_size=True - ) - printer.text('========================\n') - text = 'MESA: ' + str(d['table'] + "\n") - printer.text(text) - printer.text('========================\n') - printer.set(align='left', bold=False, height=6, width=6) - combination_pizza = False - pizza = 0 - for line in d["lines"]: - if line['type'] != 'title': - if combination_pizza and pizza < 2: - printer.set( - align='center', - bold=False, - height=2, - width=2, - custom_size=True) - pizza += 1 - elif pizza >= 2: - combination_pizza = False - printer.set( - align='left', - bold=False, - height=2, - width=2, custom_size=True - ) - else: - printer.set( - align='left', - bold=False, - height=2, - width=2, - custom_size=True) - - text = line['product'] + " " + str(line['quantity']) + "\n" - printer.text(text) - if line['description']: - text = line['description'] + "\n" - printer.text(text) - - if pizza == 2: - printer.ln() - pizza = 0 - combination_pizza = False + def _get_printer(self): + """Obtiene la instancia de impresora según la configuración""" + if self.address: + printer = PrinterFactory( + type_="network", host=self.address)._get_printer() + printer.open() + return printer else: - printer.set( - align='left', bold=True, height=2, width=2, custom_size=True + return PrinterFactory(type_="dummy")._get_printer() + + def _close_printer(self): + """Cierra la conexión con la impresora si es necesaria""" + if self.address and hasattr(self.printer, 'close'): + self.printer.close() + + def _get_current_time_bogota(self) -> str: + """Obtiene la hora actual en formato America/Bogota""" + format_ = "%Y-%m-%d %H:%M:%S" + bogota_tz = pytz.timezone('America/Bogota') + return datetime.now(bogota_tz).strftime(format_) + + +class BillPrinter(TicketPrinter): + """Manejador específico para facturas""" + + def print_bill(self, data: Dict[str, Any], waiter: Optional[str] = None): + """Imprime una factura""" + try: + self._print_bill_content(data, waiter) + if self.address: + self.printer.cut() + else: + self._print_dummy_output() + finally: + self._close_printer() + + def _print_bill_content(self, data: Dict[str, Any], waiter: Optional[str]): + """Genera el contenido de la factura""" + self._print_header(data) + self._print_invoice_info(data) + self._print_customer_info(data) + self._print_items(data) + self._print_totals(data) + self._print_payments(data) + self._print_qr_code(data) + self._print_footer(waiter) + + def _print_header(self, data: Dict[str, Any]): + """Imprime el encabezado de la factura""" + self.printer.set(align='center', bold=False, height=1, width=1) + self.printer.text(f"{data['shop_name']}\n") + self.printer.text(f"{data['shop_nit']}\n") + self.printer.text(f"{data['shop_address']}\n") + self._print_separator() + + def _print_invoice_info(self, data: Dict[str, Any]): + """Imprime información de facturación""" + self.printer.set(align='left', bold=False, height=1, width=1) + self.printer.textln(data['state']) + + if data.get('invoice') and data['invoice'].get('resolution'): + resolution = data['invoice']['resolution'] + resolution_number = resolution['resolution_number'] + self.printer.textln( + f"Resolucion de Facturacion # {resolution_number}\n" + f"Valida desde {resolution['valid_date_time_from']} " + f"hasta {resolution['valid_date_time_to']}" ) - printer.text("\nPIZZA COMBINADA\n") - combination_pizza = True - pizza = 0 + self.printer.ln() + self.printer.textln( + f"Factura #: {data['invoice']['invoice_number']}") - # if d["deleted_lines"]: - # for line in d["deleted_lines"]: - # text = line['product'] + " " + str( - # line['quantity']) + " " + str( - # line['unit']) - # printer.text(text) + def _print_customer_info(self, data: Dict[str, Any]): + """Imprime información del cliente""" + self.printer.text(f"Cliente: {data['party']}\n") + self.printer.text(f"CC/NIT: {data['tax_identifier_code']}\n") + self.printer.text(f"Direccion: {data['address']}\n") + self.printer.text(f"MESA: {data['table']}\n") + self._print_separator() + self.printer.ln() + + def _print_items(self, data: Dict[str, Any]): + """Imprime los items de la factura""" + for line in data["lines"]: + if line['type'] != 'title': + self.printer.text(line['product']) + self.printer.ln() + self.printer.text( + f"{line['quantity']} ${line['unit_price']}\n") + + def _print_totals(self, data: Dict[str, Any]): + """Imprime los totales""" + self.printer.set(align='right', bold=False, height=1, width=1) + self._print_separator() + self.printer.text(f"Descuento Realizado: {data['total_discount']}\n") + self.printer.text(f"Propina: {data['total_tip']}\n") + self.printer.text(f"Total (sin impuestos): {data['untaxed_amount']}\n") + self.printer.text(f"Impuestos (INC): {data['tax_amount']}\n") + self.printer.text(f"Total: {data['total']}\n") + self.printer.ln() + + def _print_payments(self, data: Dict[str, Any]): + """Imprime información de pagos""" + if 'payments' in data: + self.printer.textln("Pagos: ") + self._print_separator() + for payment in data['payments']: + self.printer.textln( + f"{payment['statement']} ${payment['amount']}") + + def _print_qr_code(self, data: Dict[str, Any]): + """Imprime el código QR si está disponible""" + if data.get("fe_cufe"): + self.printer.set(align='center', bold=False, height=1, width=1) + self._print_separator() + + qr = QRCodeGenerator(data["fe_cufe"]).generate_qr() + with tempfile.NamedTemporaryFile( + delete=True, suffix='.png', dir='/tmp') as temp_file: + temp_filename = temp_file.name + qr.save(temp_filename) + self.printer.image(temp_filename) + + self.printer.set(align='left', bold=False, height=1, width=1) + self.printer.textln(f"CUFE: {data['cufe']}") + + def _print_footer(self, waiter: Optional[str]): + """Imprime el pie de página""" + self.printer.set(align='center', bold=False, height=1, width=1) + self._print_separator() + self.printer.text("Sigue nuestras redes sociales\n") + self.printer.text("@bicipizza\n") + self.printer.text("Recuerde que la propina es voluntaria.\n") + self.printer.text("Gracias por visitarnos, vuelva pronto.\n") + self.printer.text("SOFTWARE POTENCIADO POR ONECLUSTER.ORG.\n") + self.printer.text(f"{self._get_current_time_bogota()}\n") + + if waiter: + self.printer.text("Atendido Por: \n") + self.printer.text(f"{waiter}\n") + + def _print_separator(self): + """Imprime una línea separadora""" + self.printer.textln('================================================') + + def _print_dummy_output(self): + """Muestra la salida cuando no hay impresora real""" + ticket_content = self.printer.output + sys.stdout.write(ticket_content.decode('utf-8', errors='ignore')) +class CustomerOrderPrinter(TicketPrinter): + """Manejador específico para órdenes de cliente""" + + def print_order(self, data: Dict[str, Any], waiter: Optional[str] = None): + """Imprime una orden de cliente""" + try: + self._print_order_content(data, waiter) + if self.address: + self.printer.cut() + else: + self._print_dummy_output() + finally: + self._close_printer() + + def _print_order_content( + self, data: Dict[str, Any], waiter: Optional[str]): + """Genera el contenido de la orden""" + self._print_header(waiter) + self._print_table_info(data) + self._print_order_items(data) + + def _print_header(self, waiter: Optional[str]): + """Imprime el encabezado de la orden""" + self.printer.set(align='center', bold=False, height=1, width=1) + self.printer.text(f"{self._get_current_time_bogota()}\n") + + if waiter: + self.printer.text("Pedido Por: \n") + self.printer.text(f"{waiter}\n") + + def _print_table_info(self, data: Dict[str, Any]): + """Imprime información de la mesa""" + self.printer.set( + align='center', bold=False, height=2, width=2, custom_size=True) + self.printer.text('========================\n') + self.printer.text(f"MESA: {data['table']}\n") + self.printer.text('========================\n') + + def _print_order_items(self, data: Dict[str, Any]): + """Imprime los items de la orden""" + combination_pizza = False + pizza_count = 0 + + for line in data["lines"]: + if line['type'] != 'title': + self._set_item_format(combination_pizza, pizza_count) + + text = f"{line['product']} {line['quantity']}\n" + self.printer.text(text) + + if line.get('description'): + self.printer.text(f"{line['description']}\n") + + if combination_pizza: + pizza_count += 1 + if pizza_count >= 2: + self.printer.ln() + pizza_count = 0 + combination_pizza = False + else: + self._print_combined_pizza_header() + combination_pizza = True + pizza_count = 0 + + def _set_item_format(self, is_combined: bool, pizza_count: int): + """Configura el formato según el tipo de item""" + if is_combined and pizza_count < 2: + self.printer.set( + align='center', + bold=False, + height=2, + width=2, + custom_size=True) + else: + self.printer.set( + align='left', bold=False, height=2, width=2, custom_size=True) + + def _print_combined_pizza_header(self): + """Imprime el encabezado para pizza combinada""" + self.printer.set( + align='left', bold=True, height=2, width=2, custom_size=True) + self.printer.text("\nPIZZA COMBINADA\n") + + def _print_dummy_output(self): + """Muestra la salida cuando no hay impresora real""" + ticket_content = self.printer.output + sys.stdout.write(ticket_content.decode('utf-8', errors='ignore')) + + +# Funciones de interfaz para mantener compatibilidad def print_bill(data, address, waiter): - d = data - - if address: - printer = PrinterFactory(type_="network", host=address)._get_printer() - printer.open() - print_bill_format(printer, d, waiter) - printer.cut() - printer.close() - else: - printer = PrinterFactory(type_="dummy")._get_printer() - print_bill_format(printer, d, waiter) - ticket_contenido = printer.output - sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore')) + printer = BillPrinter(address) + printer.print_bill(data, waiter) def print_customer_order(data, address, waiter): - d = data - - if address: - printer = PrinterFactory(type_="network", host=address)._get_printer() - printer.open() - print_customer_order_format(printer, d, waiter) - printer.cut() - printer.close() - else: - printer = PrinterFactory(type_="dummy")._get_printer() - print_customer_order_format(printer, d, waiter) - ticket_contenido = printer.output - sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore')) + printer = CustomerOrderPrinter(address) + printer.print_order(data, waiter)