Punto 11: editor de diseno de tickets en Configuracion > Tickets

- Nuevo modelo skeen.ticket.config (AbstractModel) con layout, datos de clinica
  y logo en ir.config_parameter; _mezclar() hace que layouts viejos hereden las
  secciones nuevas; _validar() rechaza valores imposibles y textos largos.
- Endpoints GET/PUT /ticket_config, PUT /ticket_config/clinica,
  POST /ticket_config/reset (reset solo admin) con auditoria en skeen_config_audit.
- Los datos de la clinica pasan de localStorage (cada caja imprimia distinto)
  a configuracion compartida en el servidor.
- TicketDoc.tsx renderiza segun el diseno: ancho 58/80mm, fuente, tamano de letra,
  separador, mayusculas, orden de bloques y 29 secciones activables.
- TicketDesigner.tsx: editor completo con vista previa en vivo, reorden de bloques,
  textos personalizables y carga de logo.
- TicketPrint.tsx ahora es un wrapper de TicketDoc; ReciboPrint, EstadoCuentaPrint,
  RecetaPrint, CortePrint y ConteoPrint usan useClinicaPrint().
- _sale_to_dict expone telefono del paciente, medico que atendio y datos de paquete;
  pos_checkout devuelve recibido/cambio en MXN y USD para imprimirlos.
This commit is contained in:
2026-09-24 18:19:51 +00:00
parent e89bcdca3a
commit 72f12946c3
18 changed files with 1433 additions and 228 deletions

View File

@@ -521,6 +521,9 @@ export interface Sale {
name: string;
patient_id: number;
patient: string;
/** Teléfono y médico del ticket: los muestra el diseño impreso si están activos */
patient_phone?: string;
doctor?: string;
date: string;
subtotal: number;
discount: number;
@@ -1227,6 +1230,8 @@ export interface PosCheckoutResult {
sale: Sale;
payment: Payment | null;
cambio: number;
/** Efectivo recibido en caja (MXN), para imprimir "Recibido" en el ticket */
recibido?: number;
cambio_usd?: number;
rate_used?: number | null;
recibido_usd?: number;
@@ -1814,6 +1819,34 @@ export const odooApi = {
return data;
},
// ---- Diseño del ticket impreso (Configuración › Tickets) ----
async getTicketConfig(): Promise<{
status: string; layout?: unknown; clinica?: unknown; logo?: string; message?: string;
}> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/ticket_config`);
return data;
},
async putTicketConfig(layout: unknown, motivo?: string): Promise<{
status: string; layout?: unknown; message?: string;
}> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/ticket_config`,
{ layout, motivo });
return data;
},
async putTicketClinica(payload: {
clinica?: Record<string, string>; logo?: string; motivo?: string;
}): Promise<{ status: string; layout?: unknown; clinica?: unknown; logo?: string; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/ticket_config/clinica`, payload);
return data;
},
async resetTicketConfig(): Promise<{ status: string; layout?: unknown; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/ticket_config/reset`, {});
return data;
},
async getDoctorHistory(id: number): Promise<DoctorHistory> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/history`);
return data;