Corte de caja asistido (esperado vs contado, diferencias, PDF) y fotos antes/despues con comparacion en expediente

This commit is contained in:
2026-09-08 07:27:57 +00:00
parent e8690dd74e
commit 4f9a16103f
7 changed files with 672 additions and 81 deletions

View File

@@ -18,3 +18,13 @@ class SkeenPatientAdjunto(models.Model):
file = fields.Binary(string='Contenido', attachment=True, required=True)
mimetype = fields.Char(string='Tipo MIME')
notes = fields.Char(string='Notas')
# Clasificación de fotos del expediente (antes/después de un tratamiento)
antes_despues = fields.Selection([
('sin_clasificar', 'Sin clasificar'),
('antes', 'Antes'),
('despues', 'Después'),
], string='Clasificación', default='sin_clasificar')
visita_id = fields.Many2one('skeen.visita', string='Visita',
ondelete='set null')
cita_id = fields.Many2one('skeen.cita', string='Cita', ondelete='set null')
servicio_id = fields.Many2one('skeen.servicio', string='Servicio')

View File

@@ -261,9 +261,18 @@ class SkeenCorteCaja(models.Model):
total_other = fields.Float(string='Otros', compute='_compute_totals', store=True)
total_sales = fields.Float(string='Total Ventas', compute='_compute_totals', store=True)
# Cierre
# Cierre guiado: efectivo contado por el cajero y conteo informativo
# de los demás métodos.
counted_cash = fields.Float(string='Efectivo Contado', default=0.0)
counted_card = fields.Float(string='Tarjeta Contada', default=0.0)
counted_transfer = fields.Float(string='Transferencia Contada', default=0.0)
counted_other = fields.Float(string='Otros Contados', default=0.0)
closing_cash = fields.Float(string='Efectivo Cierre', default=0.0)
difference = fields.Float(string='Diferencia', compute='_compute_difference', store=True)
diff_cash = fields.Float(string='Diferencia Efectivo', compute='_compute_difference', store=True)
diff_card = fields.Float(string='Diferencia Tarjeta', compute='_compute_difference', store=True)
diff_transfer = fields.Float(string='Diferencia Transferencia', compute='_compute_difference', store=True)
diff_other = fields.Float(string='Diferencia Otros', compute='_compute_difference', store=True)
# Estado
state = fields.Selection([
@@ -276,32 +285,44 @@ class SkeenCorteCaja(models.Model):
@api.depends('date')
def _compute_totals(self):
for corte in self:
ventas = self.env['skeen.venta'].search([
('date', '=', corte.date),
('state', 'in', ('confirmed', 'paid', 'partial')),
])
# Sumar también pagos por método
pagos = self.env['skeen.pago'].search([
('payment_date', '>=', corte.date.strftime('%Y-%m-%d 00:00:00')),
('payment_date', '<=', corte.date.strftime('%Y-%m-%d 23:59:59')),
('state', '=', 'completed'),
])
cash = sum(p.amount for p in pagos if p.payment_method == 'cash')
card = sum(p.amount for p in pagos if p.payment_method in ('card', 'stripe', 'mercadopago'))
transfer = sum(p.amount for p in pagos if p.payment_method == 'transfer')
other = sum(p.amount for p in pagos if p.payment_method not in ('cash', 'card', 'stripe', 'mercadopago', 'transfer'))
corte.update(corte._totals_from_payments())
corte.total_cash = cash
corte.total_card = card
corte.total_transfer = transfer
corte.total_other = other
corte.total_sales = cash + card + transfer + other
def _totals_from_payments(self):
"""Sumatorias por método de pago para la fecha del corte."""
self.ensure_one()
pagos = self.env['skeen.pago'].search([
('payment_date', '>=', self.date.strftime('%Y-%m-%d 00:00:00')),
('payment_date', '<=', self.date.strftime('%Y-%m-%d 23:59:59')),
('state', '=', 'completed'),
])
cash = sum(p.amount for p in pagos if p.payment_method == 'cash')
card = sum(p.amount for p in pagos if p.payment_method in ('card', 'stripe', 'mercadopago'))
transfer = sum(p.amount for p in pagos if p.payment_method == 'transfer')
other = sum(p.amount for p in pagos if p.payment_method not in ('cash', 'card', 'stripe', 'mercadopago', 'transfer'))
return {
'total_cash': cash,
'total_card': card,
'total_transfer': transfer,
'total_other': other,
'total_sales': cash + card + transfer + other,
}
@api.depends('opening_cash', 'total_cash', 'closing_cash')
def refresh_totals(self):
"""Recalcula y persiste los totales del día (pagos pueden llegar después
de abrir la caja)."""
for corte in self:
corte.write(corte._totals_from_payments())
@api.depends('opening_cash', 'total_cash', 'total_card', 'total_transfer', 'total_other',
'closing_cash', 'counted_cash', 'counted_card', 'counted_transfer', 'counted_other')
def _compute_difference(self):
for corte in self:
expected = corte.opening_cash + corte.total_cash
corte.difference = corte.closing_cash - expected
corte.diff_cash = corte.counted_cash - expected
corte.diff_card = corte.counted_card - corte.total_card
corte.diff_transfer = corte.counted_transfer - corte.total_transfer
corte.diff_other = corte.counted_other - corte.total_other
@api.model_create_multi
def create(self, vals_list):

View File

@@ -1341,6 +1341,11 @@ class SkeenFrontendController(http.Controller):
'name': a.name or '',
'mimetype': a.mimetype or '',
'notes': a.notes or '',
'antes_despues': a.antes_despues,
'visita_id': a.visita_id.id if a.visita_id else None,
'cita_id': a.cita_id.id if a.cita_id else None,
'servicio_id': a.servicio_id.id if a.servicio_id else None,
'servicio': a.servicio_id.name if a.servicio_id else '',
'create_date': a.create_date.strftime('%Y-%m-%d %H:%M') if a.create_date else None,
'url': f'/skeen/frontend/v1/patients/adjuntos/{a.id}/file',
}
@@ -1383,18 +1388,50 @@ class SkeenFrontendController(http.Controller):
return json_response({'status': 'error', 'message': 'Archivo vacío'}, 400)
if len(raw) > 10 * 1024 * 1024:
return json_response({'status': 'error', 'message': 'El archivo excede 10 MB'}, 400)
adj = request.env['skeen.patient.adjunto'].sudo().create({
adj_vals = {
'partner_id': patient.id,
'kind': kind,
'name': data.get('name') or 'archivo',
'file': file_b64,
'mimetype': data.get('mimetype') or '',
'notes': data.get('notes', ''),
})
}
if data.get('antes_despues') in ('sin_clasificar', 'antes', 'despues'):
adj_vals['antes_despues'] = data['antes_despues']
for key in ('visita_id', 'cita_id', 'servicio_id'):
if data.get(key):
adj_vals[key] = int(data[key])
adj = request.env['skeen.patient.adjunto'].sudo().create(adj_vals)
# Si clasifican la foto y no dieron servicio, heredarlo de la visita
if adj.visita_id and not adj.servicio_id:
adj.servicio_id = adj.visita_id.servicio_id
return json_response({'status': 'success', 'adjunto': self._patient_adjunto_to_dict(adj)}, 201)
except Exception as e:
return json_response({'status': 'error', 'message': str(e)}, 500)
@http.route('/skeen/frontend/v1/patients/adjuntos/<int:adj_id>', type='http', auth='none', methods=['PUT'], csrf=False)
@require_role('recepcion')
def update_patient_adjunto(self, adj_id, **kw):
"""Actualiza la clasificación antes/después (y contexto) de un adjunto."""
try:
adj = request.env['skeen.patient.adjunto'].sudo().browse(adj_id)
if not adj.exists():
return json_response({'status': 'error', 'message': 'Adjunto no encontrado'}, 404)
data = _parse_json_body()
vals = {}
if data.get('antes_despues') in ('sin_clasificar', 'antes', 'despues'):
vals['antes_despues'] = data['antes_despues']
for key in ('visita_id', 'cita_id', 'servicio_id'):
if key in data:
vals[key] = int(data[key]) if data.get(key) else None
if vals:
adj.write(vals)
if adj.visita_id and not adj.servicio_id:
adj.servicio_id = adj.visita_id.servicio_id
return json_response({'status': 'success', 'adjunto': self._patient_adjunto_to_dict(adj)})
except Exception as e:
return json_response({'status': 'error', 'message': str(e)}, 500)
@http.route('/skeen/frontend/v1/patients/adjuntos/<int:adj_id>', type='http', auth='none', methods=['DELETE'], csrf=False)
@require_role('recepcion')
def delete_patient_adjunto(self, adj_id, **kw):
@@ -2664,6 +2701,9 @@ class SkeenFrontendController(http.Controller):
if kw.get('date'):
domain.append(('date', '=', kw.get('date')))
closings = request.env['skeen.corte.caja'].sudo().search(domain, order='date desc', limit=100)
# Los pagos pueden llegar después de abrir la caja: refrescar
# los esperados de los cortes abiertos.
closings.filtered(lambda c: c.state == 'open').refresh_totals()
return json_response({
'status': 'success',
'cash_closings': [self._cash_closing_to_dict(c) for c in closings],
@@ -2693,9 +2733,24 @@ class SkeenFrontendController(http.Controller):
closing = request.env['skeen.corte.caja'].sudo().browse(closing_id)
if not closing.exists():
return json_response({'status': 'error', 'message': 'Corte no encontrado'}, 404)
closing.refresh_totals()
data = _parse_json_body()
if 'closing_cash' in data:
closing.write({'closing_cash': data['closing_cash']})
vals = {}
for key in ('closing_cash', 'counted_cash', 'counted_card',
'counted_transfer', 'counted_other'):
if key in data:
try:
vals[key] = float(data[key] or 0)
except (TypeError, ValueError):
return json_response({'status': 'error', 'message': f'Valor inválido: {key}'}, 400)
if 'notes' in data:
vals['notes'] = data.get('notes', '')
# Compatibilidad: si solo se envió el conteo, el cierre de efectivo
# registrado es el contado (difference del listado usa closing_cash).
if 'closing_cash' not in vals and 'counted_cash' in vals:
vals['closing_cash'] = vals['counted_cash']
if vals:
closing.write(vals)
closing.action_close()
return json_response({'status': 'success', 'cash_closing': self._cash_closing_to_dict(closing)})
except Exception as e:
@@ -2715,6 +2770,15 @@ class SkeenFrontendController(http.Controller):
'total_sales': c.total_sales,
'closing_cash': c.closing_cash,
'difference': c.difference,
'counted_cash': c.counted_cash,
'counted_card': c.counted_card,
'counted_transfer': c.counted_transfer,
'counted_other': c.counted_other,
'diff_cash': c.diff_cash,
'diff_card': c.diff_card,
'diff_transfer': c.diff_transfer,
'diff_other': c.diff_other,
'notes': c.notes or '',
'state': c.state,
}