Rol medico: pagina Mis ventas con KPIs y comision estimada, menu restringido y bloqueo de endpoints de dinero global
This commit is contained in:
@@ -22,6 +22,19 @@ SECRET_KEY = 'skeen.frontend.secret'
|
||||
|
||||
RANK = ROLE_RANKS
|
||||
|
||||
# Módulos de dinero globales vetados para el rol 'medico' por API: su menú ya
|
||||
# los oculta (MENU_ROLES) y aquí se cierra la vía directa con token. Su propio
|
||||
# reporte (/my/sales) no pasa por esta lista.
|
||||
MEDICO_BLOCKED_PREFIXES = (
|
||||
'/skeen/frontend/v1/reports/',
|
||||
'/skeen/frontend/v1/sales',
|
||||
'/skeen/frontend/v1/payments',
|
||||
'/skeen/frontend/v1/wallets',
|
||||
'/skeen/frontend/v1/pos/',
|
||||
'/skeen/frontend/v1/cash_closings',
|
||||
'/skeen/frontend/v1/doctors/',
|
||||
)
|
||||
|
||||
|
||||
def _cors_headers():
|
||||
return {
|
||||
@@ -118,6 +131,8 @@ def require_role(min_role='lectura'):
|
||||
return _json({'status': 'error', 'message': 'No autenticado'}, 401)
|
||||
if user.role_rank() < required:
|
||||
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
if user.role == 'medico' and request.httprequest.path.startswith(MEDICO_BLOCKED_PREFIXES):
|
||||
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
||||
request.frontend_user = user
|
||||
return func(self, *args, **kw)
|
||||
return wrapper
|
||||
|
||||
@@ -1843,6 +1843,10 @@ class SkeenFrontendController(http.Controller):
|
||||
venta, solo a la cita). Stripe/MercadoPago no cuentan como tarjeta.
|
||||
"""
|
||||
try:
|
||||
# Rol medico: la agenda necesita la lista de doctores, pero los KPIs
|
||||
# de dinero por médico son del módulo Médicos (admin/recepción).
|
||||
fu = current_user(request.env)
|
||||
hide_money = bool(fu and fu.role == 'medico')
|
||||
doctors = request.env['hr.employee'].sudo().search([('active', '=', True)], limit=100)
|
||||
# Pacientes asignados por médico (primary_doctor_id)
|
||||
counts = {}
|
||||
@@ -1904,13 +1908,13 @@ class SkeenFrontendController(http.Controller):
|
||||
'job_title': d.job_title or '',
|
||||
'work_phone': d.work_phone or '',
|
||||
'work_email': d.work_email or '',
|
||||
'commission_pct': d.commission_pct or 0.0,
|
||||
'commission_pct': 0.0 if hide_money else (d.commission_pct or 0.0),
|
||||
'patient_count': counts.get(d.id, 0),
|
||||
'recomendaciones_30d': recomendaciones.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),
|
||||
'ventas_30d': 0 if hide_money else len(stats.get(d.id, {}).get('ventas', set())),
|
||||
'total_30d': 0.0 if hide_money else round(stats.get(d.id, {}).get('total', 0.0), 2),
|
||||
'total_30d_usd': None if hide_money else (round(stats.get(d.id, {}).get('total', 0.0) / rate, 2) if rate else None),
|
||||
'card_30d': 0.0 if hide_money else 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:
|
||||
@@ -2006,6 +2010,128 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/my/sales', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('medico')
|
||||
def my_sales(self, **kw):
|
||||
""""Mis ventas": ventas y comisión del médico autenticado.
|
||||
|
||||
Atribución: líneas de venta recetadas por su hr.employee
|
||||
(prescribed_by_id), misma métrica que el módulo Médicos y el reporte
|
||||
de comisiones. NUNCA acepta un doctor_id externo: siempre usa el
|
||||
usuario del token, así un médico no puede ver ventas de otro.
|
||||
|
||||
range: 30d (default) | month (este mes) | last_month (mes pasado).
|
||||
Comisión: hr.employee.commission_pct si > 0; si no, el parámetro
|
||||
global ir.config_parameter 'skeen.commission_pct' (default 0).
|
||||
"""
|
||||
try:
|
||||
user = request.frontend_user
|
||||
if not user or user.role != 'medico':
|
||||
return json_response({'status': 'error', 'message': 'Solo disponible para rol médico'}, 403)
|
||||
emp = self._linked_employee(user)
|
||||
if not emp:
|
||||
return json_response({'status': 'success', 'medico_sin_employee': True,
|
||||
'sales': [], 'kpis': {'ventas': 0, 'total_mxn': 0.0, 'total_usd': None, 'total_card': 0.0},
|
||||
'commission_pct': 0.0, 'commission_estimate': 0.0})
|
||||
|
||||
today = date.today()
|
||||
range_ = kw.get('range', '30d')
|
||||
if range_ == 'month':
|
||||
date_from, date_to = today.replace(day=1), today
|
||||
elif range_ == 'last_month':
|
||||
date_to = today.replace(day=1) - timedelta(days=1)
|
||||
date_from = date_to.replace(day=1)
|
||||
else:
|
||||
range_ = '30d'
|
||||
date_from, date_to = today - timedelta(days=30), today
|
||||
|
||||
Lineas = request.env['skeen.venta.line'].sudo()
|
||||
rows = Lineas.search_read([
|
||||
('prescribed_by_id', '=', emp.id),
|
||||
('venta_id.state', '!=', 'cancelled'),
|
||||
('venta_id.date', '>=', date_from.strftime('%Y-%m-%d')),
|
||||
('venta_id.date', '<=', date_to.strftime('%Y-%m-%d')),
|
||||
], ['venta_id', 'subtotal', 'description', 'quantity'])
|
||||
|
||||
venta_ids = list({r['venta_id'][0] for r in rows})
|
||||
ventas = request.env['skeen.venta'].sudo().browse(venta_ids)
|
||||
|
||||
# Tarjeta: exacto (card_amount) + aprox histórica por cita (misma
|
||||
# lógica que list_doctors)
|
||||
card_by_venta = {v.id: v.card_amount or 0.0 for v in ventas}
|
||||
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)
|
||||
|
||||
lines_by_venta = {}
|
||||
total_mxn = 0.0
|
||||
for r in rows:
|
||||
vid = r['venta_id'][0]
|
||||
total_mxn += r['subtotal'] or 0.0
|
||||
lines_by_venta.setdefault(vid, []).append(r)
|
||||
|
||||
def metodo_pago(v):
|
||||
paid = v.amount_paid or 0.0
|
||||
card = card_by_venta.get(v.id, 0.0)
|
||||
if paid <= 0:
|
||||
return 'Sin pagar'
|
||||
if card >= paid:
|
||||
return 'Tarjeta'
|
||||
if card > 0:
|
||||
return 'Mixto'
|
||||
return 'Efectivo/Otro'
|
||||
|
||||
sales = sorted(({
|
||||
'id': v.id,
|
||||
'name': v.name,
|
||||
'date': v.date.strftime('%Y-%m-%d') if v.date else None,
|
||||
'patient': v.partner_id.name,
|
||||
'items': [l['description'] or '' for l in lines_by_venta.get(v.id, [])],
|
||||
'amount': round(sum(l['subtotal'] or 0.0 for l in lines_by_venta.get(v.id, [])), 2),
|
||||
'payment_method': metodo_pago(v),
|
||||
'state': v.state,
|
||||
} for v in ventas), key=lambda s: (s['date'] or '', s['id']), reverse=True)
|
||||
|
||||
# TC vigente (equivalente USD)
|
||||
tc = request.env['skeen.tipo.cambio'].sudo().search([], order='date desc, id desc', limit=1)
|
||||
rate = tc.rate if tc else None
|
||||
|
||||
# % de comisión: por empleado; fallback al parámetro global
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
global_pct = float(ICP.get_param('skeen.commission_pct', 0) or 0)
|
||||
if emp.commission_pct:
|
||||
pct, pct_source = emp.commission_pct, 'empleado'
|
||||
else:
|
||||
pct, pct_source = global_pct, 'global'
|
||||
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'doctor': {'id': emp.id, 'name': emp.name},
|
||||
'range': range_,
|
||||
'date_from': date_from.strftime('%Y-%m-%d'),
|
||||
'date_to': date_to.strftime('%Y-%m-%d'),
|
||||
'kpis': {
|
||||
'ventas': len(venta_ids),
|
||||
'total_mxn': round(total_mxn, 2),
|
||||
'total_usd': round(total_mxn / rate, 2) if rate else None,
|
||||
'total_card': round(sum(card_by_venta.values()), 2),
|
||||
},
|
||||
'sales': sales,
|
||||
'commission_pct': pct,
|
||||
'commission_source': pct_source,
|
||||
'commission_estimate': round(total_mxn * pct / 100.0, 2),
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Productos (plantilla de producto de Odoo)
|
||||
# ============================================================
|
||||
|
||||
@@ -46,6 +46,7 @@ MENU_ROLES_KEY = 'skeen.menu_roles'
|
||||
DEFAULT_MENU_ROLES = {
|
||||
'/': ['admin', 'recepcion', 'medico', 'lectura'],
|
||||
'/agenda': ['admin', 'recepcion', 'medico'],
|
||||
'/mis-ventas': ['medico'],
|
||||
'/pacientes': ['admin', 'recepcion', 'medico', 'lectura'],
|
||||
'/pos': ['admin', 'recepcion'],
|
||||
'/visitas': ['admin', 'recepcion', 'medico'],
|
||||
|
||||
Reference in New Issue
Block a user