feat(pos/workshop): add 80mm thermal ticket printing for service orders
- Add generate_service_order_ticket() in thermal_printer.py with ESC/POS commands for 58mm and 80mm printers. - Add POST /pos/api/service-orders/:id/print endpoint returning raw bytes or JSON for browser rendering. - Extend printer.js with printServiceOrder() using WebUSB/Web Serial. - Add Imprimir orden button in workshop.js detail modal. - Update FASES_IMPLEMENTADAS.md.
This commit is contained in:
@@ -204,3 +204,140 @@ def _total_line(label, amount, width):
|
||||
"""Format a totals line like 'Subtotal: $1,234.56'."""
|
||||
val = f'${abs(amount):,.2f}' if amount >= 0 else f'-${abs(amount):,.2f}'
|
||||
return _format_line(label, val, width) + '\n'
|
||||
|
||||
|
||||
def generate_service_order_ticket(so_data, business_info, width=80):
|
||||
"""Generate ESC/POS bytes for a workshop service order ticket.
|
||||
|
||||
Args:
|
||||
so_data: dict with service order info:
|
||||
order_number, status, customer_name, vehicle_plate, vehicle_make,
|
||||
vehicle_model, mileage_in, fuel_level, reception_notes,
|
||||
employee_name, created_at, items[{name, part_number, quantity,
|
||||
unit_price}], labor[{description, hours, hourly_rate, total_cost}],
|
||||
estimated_cost, total
|
||||
business_info: dict with name, rfc, address
|
||||
width: 58 or 80 (mm)
|
||||
|
||||
Returns: bytes ready to send to printer
|
||||
"""
|
||||
chars = 32 if width == 58 else 48
|
||||
buf = bytearray()
|
||||
buf += INIT
|
||||
|
||||
# Header
|
||||
buf += ALIGN_CENTER
|
||||
buf += LARGE_SIZE
|
||||
buf += (business_info.get("name", "NEXUS POS") + "\n").encode("cp437", errors="replace")
|
||||
buf += NORMAL_SIZE
|
||||
if business_info.get("rfc"):
|
||||
buf += (business_info["rfc"] + "\n").encode("cp437", errors="replace")
|
||||
if business_info.get("address"):
|
||||
buf += (business_info["address"] + "\n").encode("cp437", errors="replace")
|
||||
buf += b"\n"
|
||||
|
||||
# Title
|
||||
buf += BOLD_ON + DOUBLE_HEIGHT
|
||||
buf += "ORDEN DE SERVICIO\n".encode("cp437", errors="replace")
|
||||
buf += NORMAL_SIZE + BOLD_OFF
|
||||
buf += b"\n"
|
||||
|
||||
# Order info
|
||||
buf += ALIGN_LEFT
|
||||
buf += BOLD_ON
|
||||
buf += f"Folio: {so_data.get('order_number', 'N/A')}\n".encode("cp437", errors="replace")
|
||||
buf += BOLD_OFF
|
||||
buf += f"Estado: {so_data.get('status', '')}\n".encode("cp437", errors="replace")
|
||||
buf += f"Fecha: {str(so_data.get('created_at', ''))[:19]}\n".encode("cp437", errors="replace")
|
||||
if so_data.get("employee_name"):
|
||||
buf += f"Mecanico: {so_data['employee_name']}\n".encode("cp437", errors="replace")
|
||||
buf += ("-" * chars + "\n").encode()
|
||||
|
||||
# Customer / vehicle
|
||||
if so_data.get("customer_name"):
|
||||
buf += BOLD_ON
|
||||
buf += f"Cliente: {so_data['customer_name']}\n".encode("cp437", errors="replace")
|
||||
buf += BOLD_OFF
|
||||
vehicle = " ".join(
|
||||
str(v) for v in [
|
||||
so_data.get("vehicle_plate", ""),
|
||||
so_data.get("vehicle_make", ""),
|
||||
so_data.get("vehicle_model", ""),
|
||||
] if v
|
||||
).strip()
|
||||
if vehicle:
|
||||
buf += f"Vehiculo: {vehicle}\n".encode("cp437", errors="replace")
|
||||
if so_data.get("mileage_in"):
|
||||
buf += f"Kilometraje: {so_data['mileage_in']}\n".encode("cp437", errors="replace")
|
||||
if so_data.get("fuel_level"):
|
||||
buf += f"Gasolina: {so_data['fuel_level']}\n".encode("cp437", errors="replace")
|
||||
buf += ("-" * chars + "\n").encode()
|
||||
|
||||
# Reception notes
|
||||
if so_data.get("reception_notes"):
|
||||
buf += BOLD_ON
|
||||
buf += "Falla / Observaciones:\n".encode("cp437", errors="replace")
|
||||
buf += BOLD_OFF
|
||||
for line in str(so_data["reception_notes"]).splitlines():
|
||||
buf += (line[:chars] + "\n").encode("cp437", errors="replace")
|
||||
buf += ("-" * chars + "\n").encode()
|
||||
|
||||
# Parts
|
||||
items = so_data.get("items", [])
|
||||
if items:
|
||||
buf += BOLD_ON
|
||||
buf += "REFACCIONES\n".encode("cp437", errors="replace")
|
||||
buf += BOLD_OFF
|
||||
for item in items:
|
||||
name = item.get("name", "")[:chars - 10]
|
||||
part_no = item.get("part_number", "")
|
||||
qty = item.get("quantity", 1)
|
||||
unit_price = item.get("unit_price", 0)
|
||||
line_total = qty * unit_price
|
||||
buf += f"{qty}x {name}\n".encode("cp437", errors="replace")
|
||||
if part_no:
|
||||
buf += f" #{part_no}\n".encode("cp437", errors="replace")
|
||||
buf += ALIGN_RIGHT
|
||||
buf += f"${line_total:,.2f}\n".encode("cp437", errors="replace")
|
||||
buf += ALIGN_LEFT
|
||||
buf += ("-" * chars + "\n").encode()
|
||||
|
||||
# Labor
|
||||
labor_items = so_data.get("labor", [])
|
||||
if labor_items:
|
||||
buf += BOLD_ON
|
||||
buf += "MANO DE OBRA\n".encode("cp437", errors="replace")
|
||||
buf += BOLD_OFF
|
||||
for labor in labor_items:
|
||||
desc = labor.get("description", "")[:chars - 10]
|
||||
hours = labor.get("hours", 0)
|
||||
rate = labor.get("hourly_rate", 0)
|
||||
total = labor.get("total_cost", hours * rate)
|
||||
buf += f"{desc}\n".encode("cp437", errors="replace")
|
||||
buf += f" {hours} hrs x ${rate:,.2f}\n".encode("cp437", errors="replace")
|
||||
buf += ALIGN_RIGHT
|
||||
buf += f"${total:,.2f}\n".encode("cp437", errors="replace")
|
||||
buf += ALIGN_LEFT
|
||||
buf += ("-" * chars + "\n").encode()
|
||||
|
||||
# Totals
|
||||
buf += ALIGN_RIGHT
|
||||
if items or labor_items:
|
||||
total = so_data.get("total") or sum(
|
||||
i.get("quantity", 1) * i.get("unit_price", 0) for i in items
|
||||
) + sum(labor.get("total_cost", 0) for labor in labor_items)
|
||||
buf += BOLD_ON + DOUBLE_HEIGHT
|
||||
buf += _total_line("TOTAL ESTIMADO:", total, chars).encode("cp437", errors="replace")
|
||||
buf += NORMAL_SIZE + BOLD_OFF
|
||||
if so_data.get("estimated_cost"):
|
||||
buf += _total_line("Costo estimado:", so_data["estimated_cost"], chars).encode("cp437", errors="replace")
|
||||
|
||||
# Footer
|
||||
buf += b"\n"
|
||||
buf += ALIGN_CENTER
|
||||
buf += "No es comprobante fiscal\n".encode("cp437", errors="replace")
|
||||
buf += "Nexus Autoparts POS\n".encode("cp437", errors="replace")
|
||||
buf += b"\n\n\n"
|
||||
buf += PARTIAL_CUT
|
||||
|
||||
return bytes(buf)
|
||||
|
||||
Reference in New Issue
Block a user