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

@@ -8,6 +8,7 @@ import RolesPanel from '../components/RolesPanel';
import RecetasPanel from '../components/RecetasPanel';
import CatalogoPanel from '../components/CatalogoPanel';
import AuditoriaPanel from '../components/AuditoriaPanel';
import TicketDesigner from '../components/TicketDesigner';
import ImpactoModal from '../components/ImpactoModal';
import { Card, Button, Input, PageHeader, toast } from '../components/ui';
import { odooApi, type Doctor } from '../services/odoo';
@@ -19,6 +20,7 @@ const TABS = [
{ key: 'roles', label: 'Roles' },
{ key: 'recetas', label: 'Recetas' },
{ key: 'catalogos', label: 'Catálogos' },
{ key: 'tickets', label: 'Tickets' },
{ key: 'auditoria', label: 'Auditoría' },
] as const;
@@ -51,7 +53,9 @@ const Configuracion: FC = () => {
// comisiones) queda para administración del sistema.
const esAdmin = user?.role === 'admin';
const tabsVisibles = useMemo(
() => (esAdmin ? TABS : TABS.filter((t) => t.key === 'usuarios' || t.key === 'roles')),
() => (esAdmin
? TABS
: TABS.filter((t) => t.key === 'usuarios' || t.key === 'roles' || t.key === 'tickets')),
[esAdmin],
);
const [searchParams, setSearchParams] = useSearchParams();
@@ -228,6 +232,8 @@ const Configuracion: FC = () => {
<RecetasPanel />
</Card.Body>
</Card>
) : tab === 'tickets' ? (
<TicketDesigner />
) : tab === 'auditoria' ? (
<AuditoriaPanel />
) : tab === 'catalogos' ? (

View File

@@ -1408,9 +1408,14 @@ const Pos: FC = () => {
puntos_ganados: resultado.puntos_ganados,
wallet_points: resultado.wallet_points,
cambio: resultado.cambio,
recibido: resultado.recibido,
cambio_usd: resultado.cambio_usd,
recibido_usd: resultado.recibido_usd,
rate_used: resultado.rate_used,
cajero: resultado.sale.user_login,
doctor: resultado.sale.doctor,
telefono_paciente: resultado.sale.patient_phone,
notas: resultado.sale.notes,
}}
/>
</Layout>

View File

@@ -513,6 +513,11 @@ const Tickets: FC = () => {
total: reimprimir.total,
amount_paid: reimprimir.amount_paid,
amount_due: reimprimir.amount_due,
metodo: METODO_LABELS[reimprimir.payment_method || ''] || null,
cajero: reimprimir.user_login,
doctor: reimprimir.doctor,
telefono_paciente: reimprimir.patient_phone,
notas: reimprimir.notes,
}}
/>
</Layout>

View File

@@ -906,6 +906,10 @@ const Ventas: FC = () => {
total: printSale.total,
amount_paid: printSale.amount_paid,
amount_due: printSale.amount_due,
cajero: printSale.user_login,
doctor: printSale.doctor,
telefono_paciente: printSale.patient_phone,
notas: printSale.notes,
}}
/>
</Layout>