bloque 3: lealtad configurable (motor de puntos con multiplicadores por servicio/categoria/sucursal/dia/nivel, niveles VIP con descuento y factor, expiracion, ajuste manual con motivo y reportes) + propinas en punto de venta con reparto entre el personal, desglose en ticket y corte, y opcion en la liquidacion de comisiones; modo clasico y propinas apagadas preservan el comportamiento actual
This commit is contained in:
@@ -22,6 +22,11 @@ from odoo.addons.skeen_citas.clinic_settings import (
|
||||
get_impuestos,
|
||||
get_recursos,
|
||||
get_compras,
|
||||
get_lealtad,
|
||||
get_propinas,
|
||||
)
|
||||
from odoo.addons.skeen_monedero.lealtad import (
|
||||
nivel_por_codigo as lealtad_nivel_por_codigo,
|
||||
)
|
||||
from odoo.addons.skeen_citas.glosario import (
|
||||
GLOSARIO_KEYS as GLOSARIO_KEY_SET,
|
||||
@@ -2967,15 +2972,19 @@ class SkeenFrontendController(http.Controller):
|
||||
if refs:
|
||||
for v in request.env['skeen.venta'].sudo().search([('name', 'in', list(refs))]):
|
||||
totales[v.name] = v.total
|
||||
cfg_lea = get_lealtad(request.env)
|
||||
valor = float(cfg_lea.get('valor_punto') or 1.0)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'patient_id': partner.id,
|
||||
'patient': partner.name,
|
||||
'wallet_id': monedero.id if monedero else None,
|
||||
'points': saldo,
|
||||
'equivalent_mxn': float(saldo),
|
||||
'equivalent_usd': round(saldo / rate, 2) if rate else None,
|
||||
'valor_punto': valor,
|
||||
'equivalent_mxn': float(saldo) * valor,
|
||||
'equivalent_usd': round(saldo * valor / rate, 2) if rate else None,
|
||||
'rate': rate,
|
||||
'nivel': self._lealtad_nivel_dict(cfg_lea, partner),
|
||||
'depositado': float(sum(t.points for t in (monedero.transaction_ids if monedero else []) if t.type == 'deposito')),
|
||||
'movimientos': [{
|
||||
'id': t.id,
|
||||
@@ -2993,6 +3002,255 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Lealtad: puntos, niveles y reportes
|
||||
# ============================================================
|
||||
def _lealtad_nivel_dict(self, cfg, partner):
|
||||
nivel = lealtad_nivel_por_codigo(cfg, partner.lealtad_nivel_codigo)
|
||||
return {
|
||||
'codigo': partner.lealtad_nivel_codigo or '',
|
||||
'nombre': (nivel or {}).get('nombre', '') or '',
|
||||
'descuento_pct': float((nivel or {}).get('descuento_pct') or 0.0),
|
||||
'factor_puntos': float((nivel or {}).get('factor_puntos') or 1.0),
|
||||
'color': (nivel or {}).get('color', '') or '',
|
||||
'desde': partner.lealtad_nivel_desde.strftime('%Y-%m-%d') if partner.lealtad_nivel_desde else None,
|
||||
'manual': bool(partner.lealtad_manual),
|
||||
'revisar': partner.lealtad_nivel_revisar.strftime('%Y-%m-%d') if partner.lealtad_nivel_revisar else None,
|
||||
}
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/lealtad',
|
||||
type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
@require_perm('lealtad.ver')
|
||||
def patient_lealtad(self, patient_id, **kw):
|
||||
"""Nivel VIP, saldo de puntos, valor y gasto del paciente."""
|
||||
try:
|
||||
partner = request.env['res.partner'].sudo().browse(patient_id)
|
||||
if not partner.exists() or not partner.is_patient:
|
||||
return json_response({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
|
||||
cfg = get_lealtad(request.env)
|
||||
monedero = self._monedero_de(partner, crear=False)
|
||||
saldo = monedero.points if monedero else 0
|
||||
valor = float(cfg.get('valor_punto') or 1.0)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'patient_id': partner.id,
|
||||
'patient': partner.name,
|
||||
'activo': bool(cfg.get('activo')),
|
||||
'modo': cfg.get('modo'),
|
||||
'valor_punto': valor,
|
||||
'puntos': saldo,
|
||||
'equivalente_mxn': round(saldo * valor, 2),
|
||||
'gasto_ventana': partner.lealtad_gasto,
|
||||
'nivel': self._lealtad_nivel_dict(cfg, partner),
|
||||
'niveles': cfg.get('niveles') or [],
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/lealtad/recalcular',
|
||||
type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('lealtad.ajustar')
|
||||
def patient_lealtad_recalcular(self, patient_id, **kw):
|
||||
"""Recalcula el nivel VIP (respeta la asignación manual vigente)."""
|
||||
try:
|
||||
partner = request.env['res.partner'].sudo().browse(patient_id)
|
||||
if not partner.exists() or not partner.is_patient:
|
||||
return json_response({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
|
||||
cfg = get_lealtad(request.env)
|
||||
partner._lealtad_recalcular(cfg)
|
||||
return json_response({'status': 'success',
|
||||
'nivel': self._lealtad_nivel_dict(cfg, partner),
|
||||
'gasto_ventana': partner.lealtad_gasto})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/lealtad',
|
||||
type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('lealtad.nivel')
|
||||
def patient_lealtad_set(self, patient_id, **kw):
|
||||
"""Asigna el nivel a mano con fecha de revisión (no se sobreescribe)."""
|
||||
try:
|
||||
partner = request.env['res.partner'].sudo().browse(patient_id)
|
||||
if not partner.exists() or not partner.is_patient:
|
||||
return json_response({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
|
||||
cfg = get_lealtad(request.env)
|
||||
data = _parse_json_body()
|
||||
codigo = (data.get('codigo') or '').strip()
|
||||
if codigo:
|
||||
nivel = lealtad_nivel_por_codigo(cfg, codigo)
|
||||
if not nivel:
|
||||
return json_response({'status': 'error', 'message': 'Nivel desconocido'}, 400)
|
||||
revisar = (data.get('revisar') or '').strip()
|
||||
if revisar:
|
||||
try:
|
||||
revisar_d = fields.Date.to_date(revisar)
|
||||
except Exception:
|
||||
return json_response({'status': 'error', 'message': 'Fecha de revisión inválida'}, 400)
|
||||
else:
|
||||
revisar_d = False
|
||||
usuario = getattr(request.frontend_user, 'login', '') if hasattr(request, 'frontend_user') else ''
|
||||
partner.write({
|
||||
'lealtad_nivel_codigo': codigo or False,
|
||||
'lealtad_nivel_desde': fields.Date.today() if codigo else False,
|
||||
'lealtad_manual': True,
|
||||
'lealtad_nivel_revisar': revisar_d,
|
||||
})
|
||||
self._auditar('lealtad', f'paciente:{partner.id}', 'nivel_manual',
|
||||
False, codigo, entity_label=partner.name,
|
||||
notes='Asignación manual de nivel (%s)' % usuario)
|
||||
return json_response({'status': 'success',
|
||||
'nivel': self._lealtad_nivel_dict(cfg, partner)})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/points/adjust',
|
||||
type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('gerencia')
|
||||
@require_perm('lealtad.ajustar')
|
||||
def patient_points_adjust(self, patient_id, **kw):
|
||||
"""Ajuste manual del saldo de puntos (con motivo, para auditoría)."""
|
||||
try:
|
||||
partner = request.env['res.partner'].sudo().browse(patient_id)
|
||||
if not partner.exists() or not partner.is_patient:
|
||||
return json_response({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
|
||||
data = _parse_json_body()
|
||||
try:
|
||||
delta = int(data.get('points') or 0)
|
||||
except (TypeError, ValueError):
|
||||
return json_response({'status': 'error', 'message': 'Puntos inválidos'}, 400)
|
||||
motivo = (data.get('motivo') or '').strip()
|
||||
if not delta:
|
||||
return json_response({'status': 'error', 'message': 'Indica los puntos a ajustar'}, 400)
|
||||
if not motivo:
|
||||
return json_response({'status': 'error', 'message': 'El motivo es obligatorio'}, 400)
|
||||
usuario = getattr(request.frontend_user, 'login', '') if hasattr(request, 'frontend_user') else ''
|
||||
monedero = self._monedero_de(partner, crear=True)
|
||||
antes = monedero.points
|
||||
aplicado = monedero.adjust_points(
|
||||
delta, description=motivo,
|
||||
reference=(data.get('reference') or 'AJUSTE MANUAL'),
|
||||
operator=usuario)
|
||||
self._auditar('monedero', f'paciente:{partner.id}', 'ajuste', antes,
|
||||
monedero.points, entity_label=partner.name, notes=motivo)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'points': monedero.points,
|
||||
'applied': aplicado,
|
||||
'equivalente_mxn': round(monedero.points * float(
|
||||
get_lealtad(request.env).get('valor_punto') or 1.0), 2),
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/reports/lealtad', type='http', auth='none',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
@require_perm('lealtad.ver')
|
||||
def report_lealtad(self, **kw):
|
||||
"""Puntos otorgados, canjeados, expirados y el pasivo en pesos."""
|
||||
try:
|
||||
cfg = get_lealtad(request.env)
|
||||
valor = float(cfg.get('valor_punto') or 1.0)
|
||||
Tx = request.env['skeen.monedero.transaction'].sudo()
|
||||
domain = []
|
||||
if kw.get('start'):
|
||||
domain.append(('create_date', '>=', f"{kw['start']} 00:00:00"))
|
||||
if kw.get('end'):
|
||||
domain.append(('create_date', '<=', f"{kw['end']} 23:59:59"))
|
||||
by_type = {}
|
||||
for g in Tx.read_group(domain, ['type', 'points:sum'], ['type']):
|
||||
by_type[g['type']] = g['points']
|
||||
monederos = request.env['skeen.monedero'].sudo().search([])
|
||||
activos = sum(m.points for m in monederos)
|
||||
otorgados = by_type.get('accrual', 0) + by_type.get('deposito', 0)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'modo': cfg.get('modo'),
|
||||
'valor_punto': valor,
|
||||
'otorgados': by_type.get('accrual', 0),
|
||||
'canjeados': abs(by_type.get('redemption', 0)),
|
||||
'expirados': abs(by_type.get('expiration', 0)),
|
||||
'ajustes': by_type.get('adjustment', 0),
|
||||
'depositos': by_type.get('deposito', 0),
|
||||
'by_type': by_type,
|
||||
'puntos_activos': activos,
|
||||
'pasivo_mxn': round(activos * valor, 2),
|
||||
'cuentas': len(monederos),
|
||||
'niveles': cfg.get('niveles') or [],
|
||||
'conteo_niveles': self._conteo_niveles(cfg),
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
def _conteo_niveles(self, cfg):
|
||||
"""Cuántos pacientes hay en cada nivel (para el reporte de lealtad)."""
|
||||
conteo = {}
|
||||
for codigo in [n['codigo'] for n in cfg.get('niveles') or []]:
|
||||
conteo[codigo] = 0
|
||||
partners = request.env['res.partner'].sudo().search([
|
||||
('is_patient', '=', True), ('lealtad_nivel_codigo', '!=', False)])
|
||||
for p in partners:
|
||||
if p.lealtad_nivel_codigo in conteo:
|
||||
conteo[p.lealtad_nivel_codigo] += 1
|
||||
return conteo
|
||||
|
||||
@http.route('/skeen/frontend/v1/reports/propinas', type='http', auth='none',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
@require_perm('propinas.ver')
|
||||
def report_propinas(self, **kw):
|
||||
"""Propinas por empleado y periodo (con desglose de reparto)."""
|
||||
try:
|
||||
cfg = get_propinas(request.env)
|
||||
start = kw.get('start') or (date.today().replace(day=1)).strftime('%Y-%m-%d')
|
||||
end = kw.get('end') or date.today().strftime('%Y-%m-%d')
|
||||
Reparto = request.env['skeen.propina.reparto'].sudo()
|
||||
domain = [('date', '>=', start), ('date', '<=', end)]
|
||||
grupos = Reparto.read_group(domain, ['employee_id', 'monto:sum'], ['employee_id'])
|
||||
rows = []
|
||||
for g in grupos:
|
||||
emp = g['employee_id']
|
||||
rows.append({
|
||||
'employee_id': emp[0] if emp else None,
|
||||
'empleado': emp[1] if emp else 'Sin asignar',
|
||||
'monto': g['monto'],
|
||||
'propinas': g.get('employee_id_count') or g.get('__count') or 0,
|
||||
})
|
||||
rows.sort(key=lambda r: -r['monto'])
|
||||
ventas = request.env['skeen.venta'].sudo().search([
|
||||
('date', '>=', start), ('date', '<=', end),
|
||||
('state', '!=', 'cancelled'), ('propina', '>', 0)])
|
||||
total_ventas = sum(v.propina for v in ventas)
|
||||
detalle = [{
|
||||
'venta_id': r.venta_id.id,
|
||||
'folio': r.venta_id.name,
|
||||
'date': r.date.strftime('%Y-%m-%d') if r.date else None,
|
||||
'empleado': r.employee_id.name if r.employee_id else '',
|
||||
'employee_id': r.employee_id.id if r.employee_id else None,
|
||||
'rol': r.rol,
|
||||
'monto': r.monto,
|
||||
'moneda': r.moneda or 'MXN',
|
||||
'cuenta': r.cuenta or '',
|
||||
} for r in Reparto.search(domain, order='date desc, id desc', limit=500)]
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'start': start,
|
||||
'end': end,
|
||||
'activo': bool(cfg.get('activo')),
|
||||
'reparto': cfg.get('reparto'),
|
||||
'por_empleado': rows,
|
||||
'detalle': detalle,
|
||||
'total': total_ventas,
|
||||
'total_repartido': sum(r['monto'] for r in rows),
|
||||
'total_ventas': len(ventas),
|
||||
'cuenta_propinas': cfg.get('cuenta_propinas') or '',
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/deposit', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
@require_perm('ventas.cobrar')
|
||||
@@ -5098,6 +5356,11 @@ class SkeenFrontendController(http.Controller):
|
||||
'cancel_points_restored': v.cancel_points_restored,
|
||||
'payment_method': v.payment_method or '',
|
||||
'user_login': v.user_login or '',
|
||||
'propina': v.propina or 0.0,
|
||||
'propina_moneda': v.propina_moneda or 'MXN',
|
||||
'nivel_discount': v.nivel_discount or 0.0,
|
||||
'nivel_codigo': v.nivel_codigo or '',
|
||||
'cita_id': v.cita_id.id if v.cita_id else None,
|
||||
'lines': [{
|
||||
'id': line.id,
|
||||
'service_id': line.service_id.id if line.service_id else None,
|
||||
@@ -5249,6 +5512,17 @@ class SkeenFrontendController(http.Controller):
|
||||
tax_total += bruto * tasa / 100.0
|
||||
total_est = max(0.0, base_total - discount + tax_total)
|
||||
|
||||
# Lealtad y propinas de la instancia. Con propinas apagadas y
|
||||
# lealtad clásica, `propina=0` y `nivel_discount=0`: el POS sale
|
||||
# idéntico al de siempre.
|
||||
cfg_lea = get_lealtad(request.env)
|
||||
cfg_prop = get_propinas(request.env)
|
||||
cita_id = int(data.get('cita_id', 0) or 0)
|
||||
propina_pct = data.get('propina_pct')
|
||||
propina_monto = data.get('propina')
|
||||
propina_employee_id = int(data.get('propina_employee_id', 0) or 0)
|
||||
cobrador_employee_id = int(data.get('cobrador_employee_id', 0) or 0)
|
||||
|
||||
# Moneda y tipo de cambio: validar ANTES de crear nada
|
||||
currency = data.get('currency', 'MXN')
|
||||
rate_used = None
|
||||
@@ -5278,9 +5552,47 @@ class SkeenFrontendController(http.Controller):
|
||||
'discount': discount,
|
||||
'line_ids': line_vals,
|
||||
'user_login': cobrador,
|
||||
'cita_id': cita_id or False,
|
||||
})
|
||||
venta.action_confirm()
|
||||
|
||||
# Descuento automático del nivel VIP (0 con lealtad clásica).
|
||||
nivel_pct = venta._aplicar_descuento_nivel(partner) or 0.0
|
||||
|
||||
# Propina: solo con el módulo encendido. Se calcula sobre los
|
||||
# servicios (sin impuesto no; base = subtotal - descuentos + impuesto)
|
||||
# y se guarda APARTE del importe de los servicios.
|
||||
propina = 0.0
|
||||
propina_moneda = (currency if currency in ('MXN', 'USD') else 'MXN')
|
||||
if cfg_prop.get('activo'):
|
||||
servicios_total = max(
|
||||
0.0, venta.subtotal - venta.discount - venta.nivel_discount + venta.tax)
|
||||
if propina_pct not in (None, ''):
|
||||
try:
|
||||
pct = float(propina_pct)
|
||||
except (TypeError, ValueError):
|
||||
pct = 0.0
|
||||
if pct < 0:
|
||||
pct = 0.0
|
||||
if not cfg_prop.get('permitir_personalizado') and pct <= 0:
|
||||
pct = 0.0
|
||||
propina = round(servicios_total * pct / 100.0, 2)
|
||||
elif propina_monto not in (None, ''):
|
||||
try:
|
||||
propina = round(float(propina_monto or 0), 2)
|
||||
except (TypeError, ValueError):
|
||||
propina = 0.0
|
||||
elif not cfg_prop.get('permitir_sin_propina'):
|
||||
return json_response({'status': 'error', 'message': 'Esta clínica exige registrar propina'}, 400)
|
||||
if propina < 0:
|
||||
propina = 0.0
|
||||
if propina > 0:
|
||||
venta.write({
|
||||
'propina': propina,
|
||||
'propina_moneda': propina_moneda,
|
||||
'propina_employee_id': propina_employee_id or False,
|
||||
})
|
||||
|
||||
# Paquetes vendidos: materializar el saldo del paciente. Se enlazan
|
||||
# las compras a las líneas reales de la venta para poder revertirlas.
|
||||
compras_creadas = []
|
||||
@@ -5312,14 +5624,24 @@ class SkeenFrontendController(http.Controller):
|
||||
'notes': 'Venta POS',
|
||||
})
|
||||
|
||||
# Canje de puntos como forma de pago (hasta cubrir el total)
|
||||
# Canje de puntos como forma de pago (hasta cubrir el total).
|
||||
# Clásico: cubre el 100% del servicio (comportamiento de siempre).
|
||||
# Configurable: se respeta `canje_max_pct`. La propina nunca se
|
||||
# cubre con puntos: siempre se paga en dinero.
|
||||
puntos_usados = 0
|
||||
if pay_with_points:
|
||||
total = venta.total
|
||||
puntos = int(total) if float(total).is_integer() else int(total) + 1
|
||||
import math
|
||||
valor_punto = float(cfg_lea.get('valor_punto') or 1.0) or 1.0
|
||||
base_canje = venta.total
|
||||
if cfg_lea.get('modo') == 'configurable':
|
||||
tope = venta.total * float(cfg_lea.get('canje_max_pct') or 0.0) / 100.0
|
||||
base_canje = min(base_canje, tope)
|
||||
base_canje = min(base_canje, max(0.0, venta.total - venta.propina))
|
||||
puntos = int(math.ceil(base_canje / valor_punto - 1e-9))
|
||||
puntos = min(puntos, saldo)
|
||||
monedero.redeem_points(puntos, description=f'Canje puntos venta {venta.name}', reference=venta.name)
|
||||
venta.write({'points_redeemed': puntos, 'points_discount': float(puntos)})
|
||||
venta.write({'points_redeemed': puntos,
|
||||
'points_discount': round(puntos * valor_punto, 2)})
|
||||
puntos_usados = puntos
|
||||
|
||||
# Pago monetario del restante
|
||||
@@ -5378,6 +5700,22 @@ class SkeenFrontendController(http.Controller):
|
||||
else:
|
||||
venta.action_mark_paid()
|
||||
|
||||
# Reparto de la propina entre el personal (solo con propinas
|
||||
# activas y regla distinta de 'ninguno').
|
||||
if cfg_prop.get('activo') and (venta.propina or 0) > 0:
|
||||
Emp = request.env['hr.employee'].sudo()
|
||||
cobrador_emp = Emp.browse()
|
||||
for cand in (cobrador_employee_id, propina_employee_id):
|
||||
if cand:
|
||||
rec = Emp.browse(cand)
|
||||
if rec.exists():
|
||||
cobrador_emp = rec
|
||||
break
|
||||
venta._generar_reparto_propina(
|
||||
cfg_prop,
|
||||
cobrador_employee=cobrador_emp or None,
|
||||
cobrador_login=cobrador)
|
||||
|
||||
partner.invalidate_recordset()
|
||||
# Desglose de impuestos cobrados (con impuestos apagados: base =
|
||||
# subtotal, impuesto = 0, total = el de siempre).
|
||||
@@ -5409,8 +5747,23 @@ class SkeenFrontendController(http.Controller):
|
||||
'rate_used': rate_used,
|
||||
'recibido_usd': recibido_usd,
|
||||
'puntos_usados': puntos_usados,
|
||||
'puntos_ganados': int(venta.total / 10) if venta.state == 'paid' else 0,
|
||||
'puntos_ganados': venta._wallet_points_expected() if venta.state == 'paid' else 0,
|
||||
'wallet_points': partner.wallet_points,
|
||||
'propina': venta.propina or 0.0,
|
||||
'propina_moneda': venta.propina_moneda or 'MXN',
|
||||
'nivel_discount': venta.nivel_discount or 0.0,
|
||||
'nivel': ({
|
||||
'codigo': partner.lealtad_nivel_codigo or '',
|
||||
'nombre': partner.lealtad_nivel_nombre or '',
|
||||
'descuento_pct': partner.lealtad_descuento_pct or 0.0,
|
||||
} if cfg_lea.get('modo') == 'configurable' and partner.lealtad_nivel_codigo else None),
|
||||
'reparto_propina': [{
|
||||
'employee_id': r.employee_id.id if r.employee_id else None,
|
||||
'empleado': r.employee_id.name if r.employee_id else '',
|
||||
'rol': r.rol,
|
||||
'monto': r.monto,
|
||||
'moneda': r.moneda or 'MXN',
|
||||
} for r in venta.reparto_ids],
|
||||
'stock_warnings': stock_warnings,
|
||||
'packages': [{
|
||||
'id': c.id,
|
||||
@@ -5691,6 +6044,8 @@ class SkeenFrontendController(http.Controller):
|
||||
'total_sales': sum(v.total for v in ventas),
|
||||
'total_paid': sum(v.amount_paid for v in ventas),
|
||||
'total_due': sum(v.amount_due for v in ventas),
|
||||
'total_propinas': sum(v.propina or 0.0 for v in ventas),
|
||||
'total_servicios': sum(v.total - (v.propina or 0.0) for v in ventas),
|
||||
'count': len(ventas),
|
||||
'by_day': [{'date': k, 'total': by_day_map[k]} for k in sorted(by_day_map)],
|
||||
})
|
||||
@@ -5778,6 +6133,8 @@ class SkeenFrontendController(http.Controller):
|
||||
'total': sum(v.total for v in ventas),
|
||||
'total_paid': sum(v.amount_paid for v in ventas),
|
||||
'total_due': sum(v.amount_due for v in ventas),
|
||||
'total_propinas': sum(v.propina or 0.0 for v in ventas),
|
||||
'total_servicios': sum(v.total - (v.propina or 0.0) for v in ventas),
|
||||
},
|
||||
'pagos': {'total': sum(p.amount for p in pagos), 'count': len(pagos), 'by_method': pagos_by_method},
|
||||
'inventario': {'total': len(moves), 'by_type': moves_by_type},
|
||||
@@ -6366,6 +6723,8 @@ class SkeenFrontendController(http.Controller):
|
||||
'total': sum(v.total for v in ventas),
|
||||
'total_paid': sum(v.amount_paid for v in ventas),
|
||||
'total_due': sum(v.amount_due for v in ventas),
|
||||
'total_propinas': sum(v.propina or 0.0 for v in ventas),
|
||||
'total_servicios': sum(v.total - (v.propina or 0.0) for v in ventas),
|
||||
},
|
||||
'pagos': {'total': sum(p.amount for p in pagos), 'count': len(pagos), 'by_method': pagos_by_method},
|
||||
'devoluciones': {'count': len(devoluciones), 'total': sum(v.refund_amount for v in devoluciones)},
|
||||
@@ -6561,12 +6920,19 @@ class SkeenFrontendController(http.Controller):
|
||||
for g in Tx.read_group(domain, ['type', 'points:sum'], ['type']):
|
||||
by_type[g['type']] = g['points']
|
||||
monederos = request.env['skeen.monedero'].sudo().search([])
|
||||
activos = sum(m.points for m in monederos)
|
||||
# El valor del punto sale de la lealtad; en modo clásico es 1.0, así
|
||||
# que `equivalente_mxn` conserva la cifra de siempre.
|
||||
valor = float(get_lealtad(request.env).get('valor_punto') or 1.0)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'by_type': by_type,
|
||||
'puntos_activos': sum(m.points for m in monederos),
|
||||
'puntos_activos': activos,
|
||||
'cuentas': len(monederos),
|
||||
'equivalente_mxn': sum(m.points for m in monederos) * 1.0,
|
||||
'equivalente_mxn': activos * valor,
|
||||
'valor_punto': valor,
|
||||
'expirados': abs(by_type.get('expiration', 0)),
|
||||
'pasivo_mxn': round(activos * valor, 2),
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
Reference in New Issue
Block a user