reporte concentrado estilo legacy: tabla detalle ventas x metodo x moneda + legacy_id en cita/venta + metodo saldo_favor + fix moneda USD en pagos del POS
This commit is contained in:
@@ -4809,7 +4809,7 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': f'Puntos insuficientes (saldo {saldo} pts)'}, 400)
|
||||
|
||||
method = data.get('payment_method', 'cash')
|
||||
if method not in ('cash', 'card', 'transfer', 'stripe', 'mercadopago'):
|
||||
if method not in ('cash', 'card', 'transfer', 'stripe', 'mercadopago', 'saldo_favor'):
|
||||
return json_response({'status': 'error', 'message': 'Método de pago inválido'}, 400)
|
||||
|
||||
cobrador = getattr(request.frontend_user, 'login', '') if hasattr(request, 'frontend_user') else ''
|
||||
@@ -4890,7 +4890,7 @@ class SkeenFrontendController(http.Controller):
|
||||
cambio = max(0.0, round(received - restante, 2)) if received > restante else 0.0
|
||||
if rate_used:
|
||||
cambio_usd = round(cambio / rate_used, 2)
|
||||
pago = request.env['skeen.pago'].sudo().create({
|
||||
pago_vals = {
|
||||
'partner_id': partner.id,
|
||||
'venta_id': venta.id,
|
||||
'amount': pay_amount,
|
||||
@@ -4898,7 +4898,14 @@ class SkeenFrontendController(http.Controller):
|
||||
'state': 'completed',
|
||||
'payment_date': fields.Datetime.now(),
|
||||
'provider_reference': reference,
|
||||
})
|
||||
}
|
||||
if currency == 'USD' and rate_used:
|
||||
# Cobro en dólares: registrar el pago en su moneda real
|
||||
# (USD). Los totales de la venta (amount_paid) siguen en
|
||||
# MXN; el concentrado pivotea por moneda del pago.
|
||||
pago_vals['amount'] = round(pay_amount / rate_used, 2)
|
||||
pago_vals['currency_id'] = request.env.ref('base.USD').id
|
||||
pago = request.env['skeen.pago'].sudo().create(pago_vals)
|
||||
# 'card' = terminal física; stripe/mercadopago no cuentan como tarjeta aquí
|
||||
vals = {'amount_paid': venta.amount_paid + pay_amount}
|
||||
if method == 'card':
|
||||
@@ -5743,6 +5750,156 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/reports/concentrado/detalle', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def report_concentrado_detalle(self, **kw):
|
||||
"""Concentrado estilo legacy: una fila por venta con desglose de pagos
|
||||
por método (efectivo, tarjeta, transferencia, saldo a favor, monedero)
|
||||
en cada moneda (USD / M.N.), más totales, pacientes atendidos y
|
||||
ticket promedio en MXN.
|
||||
|
||||
Filtros opcionales: doctor_ids (líneas prescribed_by_id, o el médico
|
||||
de la cita origen si la venta no trae recetado), categoria_ids
|
||||
(category de skeen.servicio), servicio_ids y vip (etiqueta VIP del
|
||||
paciente).
|
||||
"""
|
||||
try:
|
||||
start = kw.get('start', (date.today().replace(day=1)).strftime('%Y-%m-%d'))
|
||||
end = kw.get('end', date.today().strftime('%Y-%m-%d'))
|
||||
|
||||
def _parse_ints(raw):
|
||||
out = []
|
||||
for chunk in str(raw or '').split(','):
|
||||
chunk = chunk.strip()
|
||||
if chunk:
|
||||
try:
|
||||
out.append(int(chunk))
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
|
||||
doctor_ids = _parse_ints(kw.get('doctor_ids'))
|
||||
categoria_ids = [c.strip() for c in str(kw.get('categoria_ids') or '').split(',') if c.strip()]
|
||||
servicio_ids = _parse_ints(kw.get('servicio_ids'))
|
||||
solo_vip = str(kw.get('vip') or '').lower() in ('1', 'true', 'yes')
|
||||
|
||||
ventas = request.env['skeen.venta'].sudo().search([
|
||||
('state', '!=', 'cancelled'),
|
||||
('date', '>=', start), ('date', '<=', end),
|
||||
], order='date asc, id asc')
|
||||
|
||||
if doctor_ids:
|
||||
ventas = ventas.filtered(lambda v: (
|
||||
any(l.prescribed_by_id.id in doctor_ids for l in v.line_ids)
|
||||
or (not any(l.prescribed_by_id for l in v.line_ids)
|
||||
and v.cita_id and v.cita_id.doctor_id
|
||||
and v.cita_id.doctor_id.id in doctor_ids)
|
||||
))
|
||||
if servicio_ids:
|
||||
ventas = ventas.filtered(lambda v: any(
|
||||
l.service_id and l.service_id.id in servicio_ids for l in v.line_ids))
|
||||
if categoria_ids:
|
||||
ventas = ventas.filtered(lambda v: any(
|
||||
l.service_id and l.service_id.category in categoria_ids for l in v.line_ids))
|
||||
if solo_vip:
|
||||
ventas = ventas.filtered(lambda v: v.partner_id.is_vip)
|
||||
|
||||
# Pagos cobrados de las ventas (una venta puede tener varios: pago
|
||||
# mixto). Se pivotea por método de pago y por moneda del pago.
|
||||
pagos_by_venta = {}
|
||||
if ventas:
|
||||
pagos = request.env['skeen.pago'].sudo().search([
|
||||
('venta_id', 'in', ventas.ids), ('state', '=', 'completed'),
|
||||
])
|
||||
for p in pagos:
|
||||
pagos_by_venta.setdefault(p.venta_id.id, []).append(p)
|
||||
|
||||
METODOS = ('efectivo', 'tarjeta', 'transferencia', 'saldo_favor', 'monedero')
|
||||
METODO_COL = {
|
||||
'cash': 'efectivo',
|
||||
'card': 'tarjeta', 'stripe': 'tarjeta', 'mercadopago': 'tarjeta',
|
||||
'transfer': 'transferencia',
|
||||
'saldo_favor': 'saldo_favor',
|
||||
'points': 'monedero',
|
||||
}
|
||||
COLUMNAS = [f'{m}_{cur}' for m in METODOS for cur in ('usd', 'mxn')]
|
||||
|
||||
def _desglose(venta):
|
||||
vals = {c: 0.0 for c in COLUMNAS}
|
||||
for p in pagos_by_venta.get(venta.id, []):
|
||||
col = METODO_COL.get(p.payment_method)
|
||||
if not col:
|
||||
continue
|
||||
sufijo = 'usd' if p.currency_id and p.currency_id.name == 'USD' else 'mxn'
|
||||
vals[f'{col}_{sufijo}'] += p.amount
|
||||
return {c: round(v, 2) for c, v in vals.items()}
|
||||
|
||||
# Vendedor: user_login (los controladores auth='none' no dejan uid
|
||||
# en create_uid) -> nombre completo del usuario del frontend.
|
||||
logins = list({v.user_login for v in ventas if v.user_login})
|
||||
nombres_vendedor = {}
|
||||
if logins:
|
||||
for u in request.env['skeen.frontend.user'].sudo().search([('login', 'in', logins)]):
|
||||
nombres_vendedor[u.login] = u.name
|
||||
|
||||
# Tipo de cambio del día de cada venta (o el más reciente
|
||||
# disponible) para expresar el ticket promedio en MXN.
|
||||
TipoCambio = request.env['skeen.tipo.cambio'].sudo()
|
||||
tc_por_fecha = {}
|
||||
|
||||
def _tc(fecha):
|
||||
key = str(fecha)
|
||||
if key not in tc_por_fecha:
|
||||
tc = TipoCambio.search([('date', '<=', key)], order='date desc, id desc', limit=1)
|
||||
if not tc:
|
||||
tc = TipoCambio.search([], order='date desc, id desc', limit=1)
|
||||
tc_por_fecha[key] = tc.rate if tc else 0.0
|
||||
return tc_por_fecha[key]
|
||||
|
||||
rows = []
|
||||
equiv_mxn = []
|
||||
for v in ventas:
|
||||
desglose = _desglose(v)
|
||||
medico = next((l.prescribed_by_id.name for l in v.line_ids if l.prescribed_by_id), None)
|
||||
if not medico and v.cita_id and v.cita_id.doctor_id:
|
||||
medico = v.cita_id.doctor_id.name
|
||||
concepto = ', '.join(
|
||||
(l.service_id.name or l.description or '') if l.service_id
|
||||
else (l.item_id.name or l.description or '')
|
||||
for l in v.line_ids)
|
||||
fecha = v.date.strftime('%Y-%m-%d') if v.date else ''
|
||||
if fecha and v.create_date:
|
||||
fecha = f"{fecha} {v.create_date.strftime('%H:%M')}"
|
||||
rows.append({
|
||||
'folio': v.name or '',
|
||||
'fecha': fecha,
|
||||
'paciente': v.partner_id.name or '',
|
||||
'folio_dr': v.partner_id.patient_id or None,
|
||||
'medico': medico or '',
|
||||
'vendedor': nombres_vendedor.get(v.user_login, v.user_login or '') if v.user_login else '',
|
||||
'concepto': concepto,
|
||||
**desglose,
|
||||
})
|
||||
mxn = sum(desglose[f'{m}_mxn'] for m in METODOS)
|
||||
usd = sum(desglose[f'{m}_usd'] for m in METODOS)
|
||||
equiv_mxn.append(mxn + usd * _tc(v.date))
|
||||
|
||||
totals = {c: round(sum(r[c] for r in rows), 2) for c in COLUMNAS}
|
||||
pacientes_atendidos = len({v.partner_id.id for v in ventas})
|
||||
ticket_promedio = round(sum(equiv_mxn) / len(equiv_mxn), 2) if equiv_mxn else 0.0
|
||||
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'start': start,
|
||||
'end': end,
|
||||
'rows': rows,
|
||||
'totals': totals,
|
||||
'pacientes_atendidos': pacientes_atendidos,
|
||||
'ticket_promedio': ticket_promedio,
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/reports/recomendaciones', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def report_recomendaciones(self, **kw):
|
||||
|
||||
Reference in New Issue
Block a user