fase 3.3: comisiones por reglas y liquidacion (modulo skeen_comisiones: motor con reglas por servicio/categoria/articulo/membresia/paquete, monto fijo y escalonado por metas, base vendido/cobrado con y sin impuestos, neto de devoluciones, metas con bonos, ajustes y liquidacion por periodo); reporte legacy intacto con el motor apagado
This commit is contained in:
@@ -751,6 +751,110 @@ def get_compras(env):
|
||||
"""Bloque de compras vigente (copia). Fuente única para modelos y API."""
|
||||
return normalizar_compras(get_settings(env).get('compras'))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Comisiones por reglas y liquidación.
|
||||
#
|
||||
# Bloque anidado del mismo JSON de ajustes, igual que `impuestos`. La regla de
|
||||
# oro: con `activo=False` (default) NADA cambia en la instancia — el reporte de
|
||||
# comisiones de siempre (`/reports/comisiones` y `/reports/commissions`) sigue
|
||||
# devolviendo el mismo JSON y los mismos números (porcentaje plano por empleado),
|
||||
# no aparece ninguna sección nueva en el menú y no se ejecuta el motor de reglas.
|
||||
#
|
||||
# activo: enciende el motor de reglas y las liquidaciones
|
||||
# base: 'cobrado' (sobre lo pagado) | 'vendido' (subtotal)
|
||||
# incluir_impuestos: la base suma el impuesto de la línea
|
||||
# neto_de_devoluciones: resta la parte devuelta de la venta
|
||||
# comisionar_membresias: comisiona las ventas de membresía
|
||||
# comisionar_paquetes: comisiona los paquetes
|
||||
# comisionar_productos: comisiona los artículos de inventario
|
||||
# permitir_ajustes_manuales: habilita sumar/restar ajustes en la liquidación
|
||||
# dia_corte: día del mes de corte sugerido (1-28)
|
||||
# redondeo: decimales a los que se redondea la comisión
|
||||
# pago_minimo: total mínimo a liquidar (por debajo no es pagable)
|
||||
# ---------------------------------------------------------------------------
|
||||
COMISIONES_DEFAULT = {
|
||||
'activo': False,
|
||||
'base': 'cobrado',
|
||||
'incluir_impuestos': False,
|
||||
'neto_de_devoluciones': True,
|
||||
'comisionar_membresias': True,
|
||||
'comisionar_paquetes': True,
|
||||
'comisionar_productos': True,
|
||||
'permitir_ajustes_manuales': True,
|
||||
'dia_corte': 1,
|
||||
'redondeo': 2,
|
||||
'pago_minimo': 0,
|
||||
}
|
||||
|
||||
COMISIONES_BASES = ('cobrado', 'vendido')
|
||||
COMISIONES_REDONDEO_MAX = 6
|
||||
|
||||
|
||||
def normalizar_comisiones(raw):
|
||||
"""Bloque `comisiones` completo, con defaults. Tolerante: nunca lanza."""
|
||||
out = dict(COMISIONES_DEFAULT)
|
||||
if not isinstance(raw, dict):
|
||||
return out
|
||||
for k in ('activo', 'incluir_impuestos', 'neto_de_devoluciones',
|
||||
'comisionar_membresias', 'comisionar_paquetes', 'comisionar_productos',
|
||||
'permitir_ajustes_manuales'):
|
||||
out[k] = _bool(raw.get(k), COMISIONES_DEFAULT[k])
|
||||
base = str(raw.get('base') or '').strip().lower()
|
||||
out['base'] = base if base in COMISIONES_BASES else COMISIONES_DEFAULT['base']
|
||||
out['dia_corte'] = min(max(_int(raw.get('dia_corte'), COMISIONES_DEFAULT['dia_corte']), 1), 28)
|
||||
out['redondeo'] = min(max(_int(raw.get('redondeo'), COMISIONES_DEFAULT['redondeo']),
|
||||
0), COMISIONES_REDONDEO_MAX)
|
||||
out['pago_minimo'] = max(0.0, _num(raw.get('pago_minimo'), COMISIONES_DEFAULT['pago_minimo']))
|
||||
return out
|
||||
|
||||
|
||||
def validar_comisiones(raw):
|
||||
"""Valida el bloque `comisiones` de un PUT. Lanza ValueError si algo no
|
||||
cuadra (se traduce a 400). Devuelve SOLO las claves presentes y normalizadas
|
||||
para que el merge anidado no reinicie lo que el PUT no tocó."""
|
||||
if raw is None:
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError('El bloque de comisiones debe ser un objeto')
|
||||
out = {}
|
||||
for k in ('activo', 'incluir_impuestos', 'neto_de_devoluciones',
|
||||
'comisionar_membresias', 'comisionar_paquetes', 'comisionar_productos',
|
||||
'permitir_ajustes_manuales'):
|
||||
if k in raw:
|
||||
if not isinstance(raw[k], bool):
|
||||
raise ValueError('El campo %s debe ser verdadero o falso' % k)
|
||||
out[k] = raw[k]
|
||||
if 'base' in raw:
|
||||
base = str(raw['base'] or '').strip().lower()
|
||||
if base not in COMISIONES_BASES:
|
||||
raise ValueError("La base debe ser 'cobrado' o 'vendido'")
|
||||
out['base'] = base
|
||||
if 'dia_corte' in raw:
|
||||
if isinstance(raw['dia_corte'], bool) or not isinstance(raw['dia_corte'], int):
|
||||
raise ValueError('El día de corte debe ser un número entero')
|
||||
if not (1 <= raw['dia_corte'] <= 28):
|
||||
raise ValueError('El día de corte debe estar entre 1 y 28')
|
||||
out['dia_corte'] = raw['dia_corte']
|
||||
if 'redondeo' in raw:
|
||||
if isinstance(raw['redondeo'], bool) or not isinstance(raw['redondeo'], int):
|
||||
raise ValueError('El redondeo debe ser un número entero de decimales')
|
||||
if not (0 <= raw['redondeo'] <= COMISIONES_REDONDEO_MAX):
|
||||
raise ValueError('El redondeo debe estar entre 0 y %d' % COMISIONES_REDONDEO_MAX)
|
||||
out['redondeo'] = raw['redondeo']
|
||||
if 'pago_minimo' in raw:
|
||||
n = _num(raw['pago_minimo'], None)
|
||||
if n is None:
|
||||
raise ValueError('El pago mínimo debe ser un número')
|
||||
if n < 0:
|
||||
raise ValueError('El pago mínimo no puede ser negativo')
|
||||
out['pago_minimo'] = n
|
||||
return out
|
||||
|
||||
|
||||
def get_comisiones(env):
|
||||
"""Bloque de comisiones vigente (copia). Fuente única para modelos y API."""
|
||||
return normalizar_comisiones(get_settings(env).get('comisiones'))
|
||||
|
||||
# Horas de agenda de respaldo: el comportamiento de siempre (8:00-19:00).
|
||||
AGENDA_START_DEFAULT = 8.0
|
||||
AGENDA_END_DEFAULT = 19.0
|
||||
@@ -796,6 +900,7 @@ def get_settings(env):
|
||||
'membresias': normalizar_membresias(None),
|
||||
'recursos': dict(RECURSOS_DEFAULT),
|
||||
'compras': dict(COMPRAS_DEFAULT),
|
||||
'comisiones': dict(COMISIONES_DEFAULT),
|
||||
}
|
||||
raw = env['ir.config_parameter'].sudo().get_param(SETTINGS_KEY)
|
||||
if raw:
|
||||
@@ -813,6 +918,7 @@ def get_settings(env):
|
||||
out['membresias'] = normalizar_membresias(guardado.get('membresias'))
|
||||
out['recursos'] = normalizar_recursos(guardado.get('recursos'))
|
||||
out['compras'] = normalizar_compras(guardado.get('compras'))
|
||||
out['comisiones'] = normalizar_comisiones(guardado.get('comisiones'))
|
||||
return out
|
||||
|
||||
|
||||
@@ -850,6 +956,11 @@ def set_settings(env, vals):
|
||||
if bloque is not None:
|
||||
# Merge anidado: un PUT parcial (p. ej. solo activo) no borra el resto.
|
||||
actual['compras'] = {**actual['compras'], **bloque}
|
||||
if 'comisiones' in vals:
|
||||
bloque = validar_comisiones(vals['comisiones'])
|
||||
if bloque is not None:
|
||||
# Merge anidado: un PUT parcial (p. ej. solo activo) no borra el resto.
|
||||
actual['comisiones'] = {**actual['comisiones'], **bloque}
|
||||
env['ir.config_parameter'].sudo().set_param(
|
||||
SETTINGS_KEY, json.dumps(actual, ensure_ascii=False))
|
||||
return get_settings(env)
|
||||
|
||||
Reference in New Issue
Block a user