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:
@@ -4587,6 +4587,9 @@ class SkeenFrontendController(http.Controller):
|
||||
'name': v.name,
|
||||
'patient_id': v.partner_id.id,
|
||||
'patient': v.partner_id.name,
|
||||
'patient_phone': v.partner_phone or '',
|
||||
'doctor': next((l.prescribed_by_id.name for l in v.line_ids
|
||||
if l.prescribed_by_id), ''),
|
||||
'date': v.date.strftime('%Y-%m-%d') if v.date else None,
|
||||
'subtotal': v.subtotal,
|
||||
'discount': v.discount,
|
||||
@@ -4791,8 +4794,10 @@ class SkeenFrontendController(http.Controller):
|
||||
cambio = 0.0
|
||||
cambio_usd = 0.0
|
||||
recibido_usd = 0.0
|
||||
recibido = 0.0
|
||||
if restante > 0:
|
||||
received = float(data.get('amount_received', restante) or restante)
|
||||
recibido = received
|
||||
reference = ''
|
||||
if currency == 'USD':
|
||||
# Recibido en dólares: convertir a MXN con el TC vigente (ya validado arriba)
|
||||
@@ -4830,6 +4835,7 @@ class SkeenFrontendController(http.Controller):
|
||||
'sale': self._sale_to_dict(venta),
|
||||
'payment': self._payment_to_dict(pago) if pago else None,
|
||||
'cambio': cambio,
|
||||
'recibido': recibido,
|
||||
'cambio_usd': cambio_usd,
|
||||
'rate_used': rate_used,
|
||||
'recibido_usd': recibido_usd,
|
||||
@@ -6156,6 +6162,105 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Diseño de tickets (Configuración › Tickets)
|
||||
# ============================================================
|
||||
@http.route('/skeen/frontend/v1/ticket_config', type='http',
|
||||
auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def get_ticket_config(self, **kw):
|
||||
"""Layout + perfil de la clínica + logo, para el impresor del POS.
|
||||
|
||||
Lectura abierta a cualquier rol autenticado: quien imprime (recepción,
|
||||
farmacia, gerencia) necesita el mismo diseño sin depender de admin.
|
||||
"""
|
||||
try:
|
||||
return json_response({'status': 'success',
|
||||
**request.env['skeen.ticket.config'].sudo().config_completa()})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/ticket_config', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
def put_ticket_config(self, **kw):
|
||||
"""Guarda el diseño del ticket. Requiere motivo para la bitácora."""
|
||||
try:
|
||||
if request.frontend_user.role not in ('admin', 'gerencia'):
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
data = _parse_json_body()
|
||||
layout = data.get('layout')
|
||||
if layout is None:
|
||||
return json_response({'status': 'error',
|
||||
'message': 'Falta el diseño del ticket'}, 400)
|
||||
Config = request.env['skeen.ticket.config'].sudo()
|
||||
antes = Config.get_layout()
|
||||
nuevo = Config.set_layout(layout)
|
||||
self._auditar('clinica', 'ticket:layout', 'layout',
|
||||
old_value=json.dumps(antes, ensure_ascii=False)[:500],
|
||||
new_value=json.dumps(nuevo, ensure_ascii=False)[:500],
|
||||
entity_label='Diseño del ticket impreso',
|
||||
notes=(data.get('motivo') or 'actualizado').strip())
|
||||
return json_response({'status': 'success', 'layout': nuevo,
|
||||
'message': 'Diseño del ticket actualizado'})
|
||||
except ValidationError as e:
|
||||
return json_response({'status': 'error',
|
||||
'message': e.args[0] if e.args else str(e)}, 400)
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/ticket_config/clinica', type='http',
|
||||
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
def put_ticket_clinica(self, **kw):
|
||||
"""Perfil de la clínica que sale en el encabezado del ticket (y logo)."""
|
||||
try:
|
||||
if request.frontend_user.role not in ('admin', 'gerencia'):
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
data = _parse_json_body()
|
||||
Config = request.env['skeen.ticket.config'].sudo()
|
||||
clinica = None
|
||||
if data.get('clinica') is not None:
|
||||
antes = Config.get_clinica()
|
||||
clinica = Config.set_clinica(data['clinica'])
|
||||
self._auditar('clinica', 'ticket:clinica', 'perfil',
|
||||
old_value=json.dumps(antes, ensure_ascii=False)[:500],
|
||||
new_value=json.dumps(clinica, ensure_ascii=False)[:500],
|
||||
entity_label='Datos de la clínica en el ticket',
|
||||
notes=(data.get('motivo') or 'actualizado').strip())
|
||||
if 'logo' in data:
|
||||
Config.set_logo(data.get('logo') or '')
|
||||
self._auditar('clinica', 'ticket:logo', 'logo',
|
||||
new_value='presente' if (data.get('logo') or '') else 'borrado',
|
||||
entity_label='Logo del ticket')
|
||||
return json_response({'status': 'success',
|
||||
**Config.config_completa()})
|
||||
except ValidationError as e:
|
||||
return json_response({'status': 'error',
|
||||
'message': e.args[0] if e.args else str(e)}, 400)
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/ticket_config/reset', type='http',
|
||||
auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
def reset_ticket_config(self, **kw):
|
||||
"""Devuelve el diseño al formato original (el del ticket de siempre)."""
|
||||
try:
|
||||
if request.frontend_user.role != 'admin':
|
||||
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
Config = request.env['skeen.ticket.config'].sudo()
|
||||
layout = Config.reset_layout()
|
||||
self._auditar('clinica', 'ticket:layout', 'layout',
|
||||
new_value='por omisión',
|
||||
entity_label='Diseño del ticket impreso',
|
||||
notes='restaurado al formato original')
|
||||
return json_response({'status': 'success', 'layout': layout})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Reporte de cumpleañeros
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user