Médicos: ventas 30d por médico (count, total MXN/USD, pagado tarjeta)

- card_amount en ventas (pagos con terminal física)
- Stats por médico vía líneas recetadas, métrica consistente con comisiones
- 4 columnas nuevas en la página de Médicos
This commit is contained in:
2026-08-14 09:02:22 +00:00
parent 281fb34588
commit 4c3eb0b11d
4 changed files with 90 additions and 2 deletions

View File

@@ -31,6 +31,10 @@ class SkeenVenta(models.Model):
total = fields.Float(string='Total', compute='_compute_totals', store=True)
amount_paid = fields.Float(string='Pagado', default=0.0)
amount_due = fields.Float(string='Adeudo', compute='_compute_totals', store=True)
card_amount = fields.Float(
string='Pagado con tarjeta', default=0.0,
help="Monto pagado con terminal física (payment_method='card'). "
"Stripe y MercadoPago NO cuentan como tarjeta aquí.")
# Estado
state = fields.Selection([

View File

@@ -1632,6 +1632,20 @@ class SkeenFrontendController(http.Controller):
# ============================================================
@http.route('/skeen/frontend/v1/doctors', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
def list_doctors(self, **kw):
"""Médicos + estadísticas de ventas de los últimos 30 días.
- ventas_30d: ventas distintas (state != cancelled) con >=1 línea
recetada por el médico (prescribed_by_id), fecha >= hoy-30.
- total_30d: suma del subtotal de las líneas recetadas (misma métrica
que el reporte de comisiones).
- total_30d_usd: total_30d / TC vigente (None si no hay TC).
- card_30d: pagado con tarjeta física. Exacto para ventas nuevas vía
venta.card_amount (se llena en pay_sale y POS checkout). Aproximación
histórica para ventas anteriores: si la venta tiene cita_id, se suman
los pagos completed con method='card' ligados a esa cita, topados al
amount_paid de la venta (por eso es aproximada: el pago no liga a la
venta, solo a la cita). Stripe/MercadoPago no cuentan como tarjeta.
"""
try:
doctors = request.env['hr.employee'].sudo().search([('active', '=', True)], limit=100)
# Pacientes asignados por médico (primary_doctor_id)
@@ -1640,6 +1654,42 @@ class SkeenFrontendController(http.Controller):
[('is_patient', '=', True), ('primary_doctor_id', 'in', doctors.ids)],
[], ['primary_doctor_id']):
counts[g['primary_doctor_id'][0]] = g['primary_doctor_id_count']
# ---- Ventas de los últimos 30 días por médico ----
since = (date.today() - timedelta(days=30)).strftime('%Y-%m-%d')
stats = {} # doctor_id -> {'ventas': set, 'total': float}
card_by_venta = {}
ventas = request.env['skeen.venta'].sudo().search([
('state', '!=', 'cancelled'), ('date', '>=', since),
])
if ventas:
for r in request.env['skeen.venta.line'].sudo().search_read(
[('venta_id', 'in', ventas.ids), ('prescribed_by_id', '!=', False)],
['prescribed_by_id', 'venta_id', 'subtotal']):
did = r['prescribed_by_id'][0]
st = stats.setdefault(did, {'ventas': set(), 'total': 0.0})
st['ventas'].add(r['venta_id'][0])
st['total'] += r['subtotal'] or 0.0
# Tarjeta: exacto (card_amount) + aprox histórica por cita
ventas_by_id = {v.id: v for v in ventas}
for vid, v in ventas_by_id.items():
card_by_venta[vid] = v.card_amount or 0.0
citas = ventas.mapped('cita_id')
if citas:
pgroup = request.env['skeen.pago'].sudo().read_group(
[('cita_id', 'in', citas.ids), ('payment_method', '=', 'card'),
('state', '=', 'completed')],
['cita_id', 'amount:sum'], ['cita_id'])
card_by_cita = {g['cita_id'][0]: g['amount'] for g in pgroup}
for v in ventas:
if v.cita_id:
aprox = min(card_by_cita.get(v.cita_id.id, 0.0), v.amount_paid or 0.0)
card_by_venta[v.id] = max(v.card_amount or 0.0, aprox)
# TC vigente (para el equivalente USD)
tc = request.env['skeen.tipo.cambio'].sudo().search([], order='date desc, id desc', limit=1)
rate = tc.rate if tc else None
return json_response({
'status': 'success',
'doctors': [{
@@ -1650,6 +1700,10 @@ class SkeenFrontendController(http.Controller):
'work_email': d.work_email or '',
'commission_pct': d.commission_pct or 0.0,
'patient_count': counts.get(d.id, 0),
'ventas_30d': len(stats.get(d.id, {}).get('ventas', set())),
'total_30d': round(stats.get(d.id, {}).get('total', 0.0), 2),
'total_30d_usd': round(stats.get(d.id, {}).get('total', 0.0) / rate, 2) if rate else None,
'card_30d': round(sum(card_by_venta.get(vid, 0.0) for vid in stats.get(d.id, {}).get('ventas', set())), 2),
} for d in doctors],
})
except Exception as e:
@@ -1930,7 +1984,11 @@ class SkeenFrontendController(http.Controller):
'state': 'completed',
'payment_date': fields.Datetime.now(),
})
sale.write({'amount_paid': sale.amount_paid + amount})
# 'card' = terminal física; stripe/mercadopago no cuentan como tarjeta aquí
vals = {'amount_paid': sale.amount_paid + amount}
if method == 'card':
vals['card_amount'] = sale.card_amount + amount
sale.write(vals)
if sale.amount_paid >= sale.total:
sale.action_mark_paid()
else:
@@ -2133,7 +2191,11 @@ class SkeenFrontendController(http.Controller):
'payment_date': fields.Datetime.now(),
'provider_reference': reference,
})
venta.write({'amount_paid': venta.amount_paid + pay_amount})
# 'card' = terminal física; stripe/mercadopago no cuentan como tarjeta aquí
vals = {'amount_paid': venta.amount_paid + pay_amount}
if method == 'card':
vals['card_amount'] = venta.card_amount + pay_amount
venta.write(vals)
if venta.amount_paid >= venta.total:
venta.action_mark_paid()
else: