1 Commits

Author SHA1 Message Date
d7e682c1ec Configuration api print Calzabana 2025-09-21 14:32:58 -05:00
19 changed files with 723 additions and 477 deletions

0
Api/__init__.py Normal file
View File

View File

@@ -1,7 +0,0 @@
from formats import BillPrinter
from tools import load_json
if __name__ == "__main__":
data = load_json('./test/fixtures/bill.json')
printer = BillPrinter("")
printer.print_bill(data, "")

View File

@@ -1,7 +0,0 @@
from formats import CustomerOrderPrinter
from tools import load_json
if __name__ == "__main__":
data = load_json('./test/fixtures/customer_order.json')
printer = CustomerOrderPrinter("")
printer.print_order(data, "")

View File

@@ -1,256 +0,0 @@
#!/usr/bin/env python3
import sys
import tempfile
from datetime import datetime
from typing import Dict, Any, Optional
import pytz
from qr_generator import QRCodeGenerator
from printer_factory import PrinterFactory
class TicketPrinter:
"""Clase principal para manejar la impresión de tickets"""
def __init__(self, address: Optional[str] = None):
self.address = address
self.printer = self._get_printer()
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:
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']}"
)
self.printer.ln()
self.printer.textln(
f"Factura #: {data['invoice']['invoice_number']}")
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'))

BIN
Api/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,13 +1,14 @@
from fastapi import FastAPI, Response
from escpos.printer import Dummy
from escpos.printer import Network
import sys
import json import json
import logging
from typing import Dict, Any, Tuple
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from formats import BillPrinter, CustomerOrderPrinter
# Configurar logging from pydantic import BaseModel
logging.basicConfig(level=logging.INFO) from datetime import datetime, timedelta
logger = logging.getLogger(__name__) import tempfile
from PIL import Image
from .qr_generator import QRCodeGenerator
app = FastAPI( app = FastAPI(
title="Print Server FastAPI", title="Print Server FastAPI",
@@ -16,106 +17,226 @@ app = FastAPI(
) )
class PrintRequest(BaseModel): class Info(BaseModel):
content: str content: str
ip_printer: str ip_printer: str
user_name: str user_name: str
class PrintResponse(BaseModel): def print_bill(data, address, waiter):
message: str d = data
success: bool # Crea una instancia de la impresora ficticia
print_type: str printer = Network(str(address))
# Abrir el cajo solo las impreroras que tiene esa funcion tambien puede ser 1
printer.cashdraw(2)
# printer.open()
# Imprimer logo
# printer = Dummy()
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
printer.image("/app/assets/logo.png")
printer.ln()
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')
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 = "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("Gracias por visitarnos, vuelva pronto.\n")
printer.text("SOFTWARE POTENCIADO POR ONECLUSTER.COM.CO\n")
adjusted_time = datetime.now() - timedelta(hours=5)
format_date_time = adjusted_time.strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\n')
if waiter:
printer.text("Atendido Por: \n")
printer.text(str(waiter)+'\n')
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
ticket_contenido = printer.output
# Imprime el contenido en la consola
sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore'))
def _parse_request_data(info: PrintRequest) -> Tuple[Dict[str, Any], str, str]: def print_customer_order(data, address, waiter):
"""Parse and validate request data with better error handling""" d = data
try: # Crea una instancia de la impresora ficticia
content_clean = info.content.replace("'", "\"") printer = Network(str(address))
data = json.loads(content_clean) printer.open()
# printer = Dummy()
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
format_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\n')
logger.info( if waiter:
f"Print request from {info.user_name} to {info.ip_printer}") printer.text("Pedido Por: \n")
return data, info.ip_printer, info.user_name printer.text(str(waiter)+'\n')
printer.set(
except json.JSONDecodeError as e: align='center', bold=False, height=2, width=2, custom_size=True
logger.error(f"JSON decode error: {str(e)}")
raise HTTPException(status_code=400, detail="Invalid JSON format")
except ValueError as e:
logger.error(f"Validation error: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Unexpected error parsing request: {str(e)}")
raise HTTPException(status_code=400, detail="Error processing request")
@app.post("/print_bill", response_model=PrintResponse)
def print_ticket_bill(info: PrintRequest) -> PrintResponse:
"""Print bill ticket"""
try:
data, address, waiter = _parse_request_data(info)
printer = BillPrinter(address)
printer.print_bill(data, waiter)
logger.info(f"Bill printed successfully for {waiter}")
return PrintResponse(
message="✅ Impresión de cuenta realizada exitosamente!",
success=True,
print_type="bill"
) )
except Exception as e: printer.text('========================\n')
logger.error(f"Error printing bill: {str(e)}") text = 'MESA: ' + str(d['table'] + "\n")
raise HTTPException(status_code=500, detail="Error printing bill") printer.text(text)
printer.text('========================\n')
printer.set(align='left', bold=False, height=6, width=6)
@app.post("/order_kitchen", response_model=PrintResponse) combination_pizza = False
def print_ticket_kitchen(info: PrintRequest) -> PrintResponse: pizza = 0
"""Print kitchen order""" for line in d["lines"]:
try: if line['type'] != 'title':
data, address, waiter = _parse_request_data(info) if combination_pizza and pizza < 2:
printer = CustomerOrderPrinter(address) printer.set(
printer.print_order(data, waiter) align='center',
bold=False,
logger.info(f"Kitchen order printed successfully for {waiter}") height=2,
return PrintResponse( width=2,
message="✅ Pedido de cocina impreso exitosamente!", custom_size=True)
success=True, pizza += 1
print_type="kitchen" elif pizza >= 2:
combination_pizza = False
printer.set(
align='left',
bold=False,
height=2,
width=2, custom_size=True
) )
except Exception as e: else:
logger.error(f"Error printing kitchen order: {str(e)}") printer.set(
raise HTTPException( align='left',
status_code=500, detail="Error printing kitchen order") 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)
@app.post("/order_bar", response_model=PrintResponse) if pizza == 2:
def print_ticket_bar(info: PrintRequest) -> PrintResponse: printer.ln()
"""Print bar order""" pizza = 0
try: combination_pizza = False
data, address, waiter = _parse_request_data(info) else:
printer = CustomerOrderPrinter(address) printer.set(
printer.print_order(data, waiter) align='left', bold=True, height=2, width=2, custom_size=True
logger.info(f"Bar order printed successfully for {waiter}")
return PrintResponse(
message="✅ Pedido de barra impreso exitosamente!",
success=True,
print_type="bar"
) )
except Exception as e: printer.text("\nPIZZA COMBINADA\n")
logger.error(f"Error printing bar order: {str(e)}") combination_pizza = True
raise HTTPException(status_code=500, detail="Error printing bar order") pizza = 0
# if d["deleted_lines"]:
# for line in d["deleted_lines"]:
# text = line['product'] + " " + str(
# line['quantity']) + " " + str(
# line['unit'])
# printer.text(text)
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
# ticket_contenido = printer.output
# Imprime el contenido en la consola
# sys.stdout.write(ticket_contenido.decode('utf-8', errors='replace'))
@app.get("/health") @app.post("/print_bill")
def health_check() -> Dict[str, str]: def print_ticket_bill(info: Info):
return { info = dict(info)
"status": "healthy", data = info["content"]
"service": "print_server", address = info["ip_printer"]
"version": "0.0.1" waiter = info["user_name"]
} data = json.loads(data.replace("'", "\""))
print_bill(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.get("/") @app.post("/order_kitchen")
def root() -> Dict[str, str]: def print_ticket_file_kitchen(info: Info):
return {"message": "Print Server API is running"} info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.post("/order_bar")
def print_ticket_file_bar(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)

238
Api/main2.py Normal file
View File

@@ -0,0 +1,238 @@
from fastapi import FastAPI, Response
from escpos.printer import Dummy
from escpos.printer import Network
import sys
import json
from pydantic import BaseModel
from datetime import datetime, timedelta
import tempfile
from .qr_generator import QRCodeGenerator
app = FastAPI(
title="Print Server FastAPI",
description="Server that receive request for printing",
version="0.0.1"
)
class Info(BaseModel):
content: str
ip_printer: str
user_name: str
def print_bill(data, address, waiter):
d = data
# Crea una instancia de la impresora ficticia
printer = Network(str(address))
# Abrir el cajo solo las impreroras que tiene esa funcion tambien puede ser 1
printer.cashdraw(2)
# printer.open()
# printer = Dummy()
# Imprime el encabezado
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')
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 = "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("Gracias por visitarnos, vuelva pronto.\n")
printer.text("SOFTWARE POTENCIADO POR ONECLUSTER.COM.CO\n")
adjusted_time = datetime.now() - timedelta(hours=5)
format_date_time = adjusted_time.strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\n')
if waiter:
printer.text("Atendido Por: \n")
printer.text(str(waiter)+'\n')
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
ticket_contenido = printer.output
# Imprime el contenido en la consola
sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore'))
def print_customer_order(data, address, waiter):
d = data
# Crea una instancia de la impresora ficticia
printer = Network(str(address))
printer.open()
# printer = Dummy()
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
format_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\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
else:
printer.set(
align='left', bold=True, height=2, width=2, custom_size=True
)
printer.text("\nPIZZA COMBINADA\n")
combination_pizza = True
pizza = 0
# if d["deleted_lines"]:
# for line in d["deleted_lines"]:
# text = line['product'] + " " + str(
# line['quantity']) + " " + str(
# line['unit'])
# printer.text(text)
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
# ticket_contenido = printer.output
# Imprime el contenido en la consola
# sys.stdout.write(ticket_contenido.decode('utf-8', errors='replace'))
@app.post("/print_bill")
def print_ticket_bill(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_bill(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.post("/order_kitchen")
def print_ticket_file_kitchen(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.post("/order_bar")
def print_ticket_file_bar(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)

View File

@@ -1,17 +0,0 @@
from escpos.printer import Dummy, Network
class PrinterFactory:
def __init__(self, type_="dummy", host="0.0.0.0", port=9100):
self.type_ = type_
self.host = host
self.port = port
def _get_printer(self):
if self.type_ == "dummy":
return Dummy()
elif self.type_ == "network":
return Network(self.host, self.port)
else:
raise ValueError(f"Tipo de impresora no soportado: {self.type_}")

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from qr_generator import QRCodeGenerator from ..qr_generator import QRCodeGenerator
def test_generate_qr(): def test_generate_qr():

View File

@@ -1,33 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from main import app from ..main import app
from tools import load_json
import json import json
client = TestClient(app) client = TestClient(app)
def load_json(file_path):
with open(file_path, 'r') as filejson:
return json.load(filejson)
def test_print_bill(): def test_print_bill():
test_info = { test_info = {
"content": str( "content": str(
json.dumps( json.dumps(
load_json('test/fixtures/bill.json') load_json('test/fixtures/bill.json')
)), )),
"ip_printer": "", "ip_printer": "192.168.1.105",
"user_name": "Juan" "user_name": "Juan"
} }
response = client.post("/print_bill", json=test_info) response = client.post("/print_bill", json=test_info)
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
response_data = response.json()
expected_data = {
"message": "✅ Impresión de cuenta realizada exitosamente!",
"success": True,
"print_type": "bill"
}
assert response_data == expected_data
def test_print_customer_order(): def test_print_customer_order():
@@ -36,21 +33,14 @@ def test_print_customer_order():
json.dumps( json.dumps(
load_json('test/fixtures/customer_order.json') load_json('test/fixtures/customer_order.json')
)), )),
"ip_printer": "", "ip_printer": "192.168.1.100",
"user_name": "Juan" "user_name": "Juan"
} }
response = client.post("/order_kitchen", json=test_info) response = client.post("/order_kitchen", json=test_info)
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
response_data = response.json()
expected_data = {
"message": "✅ Pedido de cocina impreso exitosamente!",
"success": True,
"print_type": "kitchen"
}
assert response_data == expected_data
def test_print_customer_order_deleted_lines(): def test_print_customer_order_deleted_lines():
@@ -60,22 +50,14 @@ def test_print_customer_order_deleted_lines():
load_json( load_json(
'test/fixtures/customer_order_deleted_lines.json') 'test/fixtures/customer_order_deleted_lines.json')
)), )),
"ip_printer": "", "ip_printer": "192.168.1.110",
"user_name": "Juan" "user_name": "Juan"
} }
response = client.post("/order_kitchen", json=test_info) response = client.post("/order_kitchen", json=test_info)
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
response_data = response.json()
expected_data = {
"message": "✅ Pedido de cocina impreso exitosamente!",
"success": True,
"print_type": "kitchen"
}
assert response_data == expected_data
def test_print_bar_order(): def test_print_bar_order():
@@ -84,18 +66,11 @@ def test_print_bar_order():
json.dumps( json.dumps(
load_json('test/fixtures/customer_order.json') load_json('test/fixtures/customer_order.json')
)), )),
"ip_printer": "", "ip_printer": "192.168.1.110",
"user_name": "Juan" "user_name": "Juan"
} }
response = client.post("/order_bar", json=test_info) response = client.post("/order_bar", json=test_info)
assert response.status_code == 200 assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
response_data = response.json()
expected_data = {
"message": "✅ Pedido de barra impreso exitosamente!",
"success": True,
"print_type": "bar"
}
assert response_data == expected_data

View File

@@ -1,41 +0,0 @@
import unittest
from unittest.mock import patch, MagicMock
from escpos.printer import Dummy
from printer_factory import PrinterFactory
class TestPrinterFactory(unittest.TestCase):
def setUp(self):
self.factory_dummy = PrinterFactory(type_="dummy")
self.factory_network = PrinterFactory(
type_="network", host="192.168.1.100")
def test_create_dummy_printer(self):
"""Test creación de impresora dummy"""
printer = self.factory_dummy._get_printer()
self.assertIsInstance(printer, Dummy)
@patch('printer_factory.Network')
def test_create_network_printer(self, mock_network):
"""Test creación de impresora de red con mock"""
mock_printer_instance = MagicMock()
mock_network.return_value = mock_printer_instance
printer = self.factory_network._get_printer()
mock_network.assert_called_once_with("192.168.1.100", 9100)
self.assertEqual(printer, mock_printer_instance)
def test_create_unknown_printer_type(self):
"""Test para tipo de impresora desconocido"""
with self.assertRaises(ValueError):
PrinterFactory(type_="invalid_type")._get_printer()
if __name__ == '__main__':
unittest.main()

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env python3
import json
def load_json(file_path):
with open(file_path, 'r') as filejson:
return json.load(filejson)

View File

@@ -1,18 +1,23 @@
# Usa una imagen base de Python
FROM python:3.11-slim FROM python:3.11-slim
# Establece el directorio de trabajo
WORKDIR /app WORKDIR /app
# Copia el archivo de requerimientos y lo instala # Instala dependencias para manejo de imágenes
COPY ./requirements.txt /tmp/ RUN apt-get update && apt-get install -y \
RUN pip install --no-cache-dir -r /tmp/requirements.txt libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Copia el código fuente # Crea directorio para assets
RUN mkdir -p /app/assets
# Copia requirements e instala dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copia el resto del código
COPY . . COPY . .
# Expone el puerto que usará FastAPI
EXPOSE 8000 EXPOSE 8000
# Comando de arranque
CMD ["uvicorn", "Api.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "Api.main:app", "--host", "0.0.0.0", "--port", "8000"]

0
assets/logo.png Normal file
View File

View File

@@ -1,9 +1,17 @@
version: '3.8'
services: services:
escpos: escpos:
build: . build:
context: .
dockerfile: Dockerfile
container_name: escpos container_name: escpos
command: uvicorn Api.main:app --host 0.0.0.0 --port 8000 --reload command: uvicorn Api.main:app --host 0.0.0.0 --port 8000 --reload
restart: always
volumes: volumes:
- .:/app - .:/app
- ./logo.png:/app/assets/logo.png # Monta el logo específicamente
ports: ports:
- "8050:8000" - "8050:8000"
environment:
- LOGO_PATH=/app/assets/logo.png # Variable de entorno opcional

BIN
logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

235
main.py Normal file
View File

@@ -0,0 +1,235 @@
from fastapi import FastAPI, Response
from escpos.printer import Dummy
from escpos.printer import Network
import sys
import json
from pydantic import BaseModel
from datetime import datetime
import tempfile
from .qr_generator import QRCodeGenerator
app = FastAPI(
title="Print Server FastAPI",
description="Server that receive request for printing",
version="0.0.1"
)
class Info(BaseModel):
content: str
ip_printer: str
user_name: str
def print_bill(data, address, waiter):
d = data
# Crea una instancia de la impresora ficticia
printer = Network(str(address))
# printer.open()
# printer = Dummy()
# Imprime el encabezado
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')
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 = "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("Gracias por visitarnos, vuelva pronto.\n")
printer.text("SOFTWARE POTENCIADO POR ONECLUSTER.ORG.\n")
format_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\n')
if waiter:
printer.text("Atendido Por: \n")
printer.text(str(waiter)+'\n')
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
ticket_contenido = printer.output
# Imprime el contenido en la consola
sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore'))
def print_customer_order(data, address, waiter):
d = data
# Crea una instancia de la impresora ficticia
printer = Network(str(address))
printer.open()
# printer = Dummy()
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
format_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
printer.text(str(format_date_time)+'\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
else:
printer.set(
align='left', bold=True, height=2, width=2, custom_size=True
)
printer.text("\nPIZZA COMBINADA\n")
combination_pizza = True
pizza = 0
# if d["deleted_lines"]:
# for line in d["deleted_lines"]:
# text = line['product'] + " " + str(
# line['quantity']) + " " + str(
# line['unit'])
# printer.text(text)
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
# ticket_contenido = printer.output
# Imprime el contenido en la consola
# sys.stdout.write(ticket_contenido.decode('utf-8', errors='replace'))
@app.post("/print_bill")
def print_ticket_bill(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_bill(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.post("/order_kitchen")
def print_ticket_file_kitchen(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@app.post("/order_bar")
def print_ticket_file_bar(info: Info):
info = dict(info)
data = info["content"]
address = info["ip_printer"]
waiter = info["user_name"]
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)

View File

@@ -5,4 +5,3 @@ pytest
escpos escpos
qrcode qrcode
flake8 flake8
pytz