Corte de caja asistido (esperado vs contado, diferencias, PDF) y fotos antes/despues con comparacion en expediente
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user