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:
@@ -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([
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user