Punto 7: gerencia puede corregir tickets sin cancelarlos (lineas, precios, descuento y notas) con ajuste automatico de monedero e inventario

This commit is contained in:
2026-09-24 17:53:22 +00:00
parent dcf2c367f1
commit e89bcdca3a
7 changed files with 881 additions and 4 deletions

View File

@@ -4360,6 +4360,61 @@ class SkeenFrontendController(http.Controller):
except Exception as e:
return json_response({'status': 'error', 'message': str(e)}, 500)
@http.route('/skeen/frontend/v1/sales/<int:sale_id>', type='http',
auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
@require_role('recepcion')
@forbid_roles('marketing')
def update_sale(self, sale_id, **kw):
"""Corrige un ticket ya emitido (gerencia / administrador).
Recepción a veces cobra mal — un servicio equivocado, una cantidad o un
precio alzado, falta un concepto. Antes la única salida era cancelar y
rehacer, que revierte el cobro completo. Aquí se modifica el ticket
conservando el pago registrado.
`lines` reemplaza la lista de conceptos (con `id` para editar, sin `id`
para alta; las que no aparecen se eliminan). Se requiere `motivo` y
queda en el chatter del ticket y en la bitácora de auditoría.
"""
try:
if request.frontend_user.role not in ('admin', 'gerencia'):
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
sale = request.env['skeen.venta'].sudo().browse(sale_id)
if not sale.exists():
return json_response({'status': 'error', 'message': 'Venta no encontrada'}, 404)
data = _parse_json_body()
motivo = (data.get('motivo') or data.get('reason') or '').strip()
if not motivo:
return json_response({'status': 'error',
'message': 'Indica el motivo de la corrección'}, 400)
antes = {'total': sale.total, 'conceptos': len(sale.line_ids),
'state': sale.state}
result = sale.action_edit(
reason=motivo,
lines=data.get('lines'),
discount=data.get('discount'),
tax=data.get('tax'),
notes=data.get('notes'),
date=data.get('date'),
editor=request.frontend_user.login,
)
self._auditar('ventas', f'venta:{sale.id}', 'edicion_ticket',
old_value='%.2f · %d conceptos · %s' % (
antes['total'], antes['conceptos'], antes['state']),
new_value='%.2f · %d conceptos · %s' % (
sale.total, len(sale.line_ids), sale.state),
entity_label=f'{sale.name} · {sale.partner_id.name}',
notes=motivo)
return json_response({'status': 'success',
'sale': self._sale_con_pagos(sale),
'result': result})
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/sales/<int:sale_id>/pay', type='http', auth='none', methods=['POST'], csrf=False)
@require_role('recepcion')
@forbid_roles('marketing')
@@ -4410,7 +4465,7 @@ class SkeenFrontendController(http.Controller):
amount = data.get('amount') # None o <=0 => devolución total de lo pagado
reason = data.get('reason', '')
result = sale.action_refund(amount=amount, reason=reason)
return json_response({'status': 'success', 'sale': self._sale_to_dict(sale), 'result': result})
return json_response({'status': 'success', 'sale': self._sale_con_pagos(sale), 'result': result})
except Exception as e:
return json_response({'status': 'error', 'message': str(e)}, 500)
@@ -4438,7 +4493,7 @@ class SkeenFrontendController(http.Controller):
cancelled_by=request.frontend_user.login,
restore_stock=bool(data.get('restore_stock', True)),
)
return json_response({'status': 'success', 'sale': self._sale_to_dict(sale), 'result': result})
return json_response({'status': 'success', 'sale': self._sale_con_pagos(sale), 'result': result})
except Exception as e:
request.env.cr.rollback()
return json_response({'status': 'error', 'message': str(e)}, 500)
@@ -4518,6 +4573,14 @@ class SkeenFrontendController(http.Controller):
except Exception as e:
return json_response({'status': 'error', 'message': str(e)}, 500)
def _sale_con_pagos(self, v):
# _sale_to_dict + los pagos ligados: el modal de Tickets los muestra
# y tras una corrección hay que devolverlos o parecería que el cobro
# se perdió.
pagos = [self._payment_to_dict(p)
for p in request.env['skeen.pago'].sudo().search([('venta_id', '=', v.id)])]
return {**self._sale_to_dict(v), 'pagos': pagos}
def _sale_to_dict(self, v):
return {
'id': v.id,
@@ -4557,6 +4620,9 @@ class SkeenFrontendController(http.Controller):
'is_prescribed': line.is_prescribed,
'prescribed_by_id': line.prescribed_by_id.id if line.prescribed_by_id else None,
'prescribed_by': line.prescribed_by_id.name if line.prescribed_by_id else None,
'paquete_id': line.paquete_id.id if line.paquete_id else None,
'paquete': line.paquete_id.name if line.paquete_id else '',
'sesiones': line.paquete_id.sessions if line.paquete_id else 0,
} for line in v.line_ids],
}
@@ -6081,6 +6147,10 @@ class SkeenFrontendController(http.Controller):
'total': total,
'page': page,
'page_size': page_size,
# Los módulos disponibles salen de la selección del modelo para
# que el filtro del frontend nunca se desincronice.
'modulos': [{'value': k, 'label': v} for k, v in
request.env['skeen.config.audit']._fields['module'].selection],
'total_pages': (total + page_size - 1) // page_size,
})
except Exception as e: