From e89bcdca3a0664c58e14182e271ac74ce31163f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Consultor=C3=ADa=20Alcaraz=20Salazar?= Date: Thu, 24 Sep 2026 17:53:22 +0000 Subject: [PATCH] Punto 7: gerencia puede corregir tickets sin cancelarlos (lineas, precios, descuento y notas) con ajuste automatico de monedero e inventario --- frontend/src/components/AuditoriaPanel.tsx | 9 +- frontend/src/components/TicketEditor.tsx | 353 ++++++++++++++++++ frontend/src/pages/Tickets.tsx | 57 ++- frontend/src/services/odoo.ts | 41 ++ odoo-addons/skeen_ventas/models/venta.py | 350 +++++++++++++++++ .../skeen_whatsapp/controllers/frontend.py | 74 +++- .../skeen_whatsapp/models/config_audit.py | 1 + 7 files changed, 881 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/TicketEditor.tsx diff --git a/frontend/src/components/AuditoriaPanel.tsx b/frontend/src/components/AuditoriaPanel.tsx index bf766a4..9c1ba3e 100644 --- a/frontend/src/components/AuditoriaPanel.tsx +++ b/frontend/src/components/AuditoriaPanel.tsx @@ -17,6 +17,7 @@ const MODULOS = [ { value: 'agenda', label: 'Agenda' }, { value: 'paquetes', label: 'Paquetes' }, { value: 'monedero', label: 'Monedero' }, + { value: 'ventas', label: 'Tickets y cobros' }, { value: 'clinica', label: 'Clínica' }, ]; @@ -40,6 +41,9 @@ const AuditoriaPanel: FC = () => { const [loading, setLoading] = useState(true); const [modulo, setModulo] = useState(''); + // Se reemplaza con la lista que devuelve el API (viene de la selección del + // modelo); MODULOS queda como fallback mientras el backend no la mande. + const [modulos, setModulos] = useState<{ value: string; label: string }[]>(MODULOS); const [usuario, setUsuario] = useState(''); const [entidad, setEntidad] = useState(''); const [desde, setDesde] = useState(''); @@ -59,6 +63,9 @@ const AuditoriaPanel: FC = () => { }); if (res.status === 'success') { setRows(res.registros); + if (res.modulos?.length) { + setModulos([{ value: '', label: 'Todos los módulos' }, ...res.modulos]); + } setTotal(res.total); setTotalPages(res.total_pages || 1); } @@ -92,7 +99,7 @@ const AuditoriaPanel: FC = () => {
setFila(i, { description: e.target.value })} + placeholder="Concepto" + className="flex-1 min-w-0 bg-transparent text-sm font-medium text-theme-heading focus:outline-none" + /> + {f.paquete && ( + + Paquete · {f.paquete} + + )} + +
+
+ setFila(i, { quantity: parseFloat(e.target.value) || 0 })} + /> + setFila(i, { price_unit: parseFloat(e.target.value) || 0 })} + /> + setBuscando(e.target.value)} + className="pl-9" + /> + {opciones.length > 0 && ( +
+ {opciones.map((o) => ( + + ))} +
+ )} + {buscando.trim() && opciones.length === 0 && ( +

Sin coincidencias en el catálogo.

+ )} +
+ + +
+ setDescuento(e.target.value)} + /> + setNotas(e.target.value)} /> +
+ +
+
+ Total actual + {money(ticket.total)} +
+
+ Total corregido + {money(nuevoTotal)} +
+ {Math.abs(dif) > 0.005 && ( +
+ Diferencia + 0 ? 'text-amber-700' : 'text-emerald-700'}> + {dif > 0 ? '+' : ''}{money(dif)} + +
+ )} +

+ Lo cobrado no cambia ({money(ticket.amount_paid)}). Si el nuevo total es mayor, el + ticket queda con saldo; si es menor, el dinero sobrante sigue en caja y hay que + devolverlo o dejarlo como saldo a favor. Los puntos del monedero y el inventario se + ajustan solos a la diferencia. +

+
+ + {hayPaquete && ( +
+ + + Este ticket vendió paquetes: sus conceptos no se editan aquí. Si el paciente ya + consumió sesiones, primero ayústalo o anula el paquete en el módulo Paquetes. + +
+ )} + +