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:
2026-10-11 12:57:19 +00:00
parent a916753ebe
commit 3c4f1b14d9
27 changed files with 3399 additions and 17 deletions

View File

@@ -1075,6 +1075,7 @@ export interface ClinicSettings {
membresias: MembresiasSettings;
recursos: RecursosSettings;
compras: ComprasSettings;
comisiones: ComisionesSettings;
}
// ---- Reservas en línea (portal público de auto-agendado) ----
@@ -1268,6 +1269,211 @@ export const COMPRAS_DEFAULT: ComprasSettings = {
permitir_compras_sin_orden: false,
};
// ---- Comisiones por reglas y liquidación ----
export interface ComisionesSettings {
/** Enciende el motor de reglas, el reporte nuevo y las liquidaciones. */
activo: boolean;
/** 'cobrado' (sobre lo pagado) o 'vendido' (sobre el subtotal de la línea). */
base: 'cobrado' | 'vendido';
/** La base suma el impuesto de la línea. */
incluir_impuestos: boolean;
/** Resta la parte devuelta de la venta. */
neto_de_devoluciones: boolean;
comisionar_membresias: boolean;
comisionar_paquetes: boolean;
comisionar_productos: boolean;
/** Habilita sumar/restar ajustes en la liquidación. */
permitir_ajustes_manuales: boolean;
/** Día del mes de corte sugerido (1-28). */
dia_corte: number;
/** Decimales a los que se redondea la comisión. */
redondeo: number;
/** Total mínimo a liquidar (por debajo no es pagable). */
pago_minimo: number;
}
export const COMISIONES_DEFAULT: ComisionesSettings = {
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,
};
export type AlcanceComision = 'global' | 'categoria' | 'servicio' | 'articulo' | 'membresia' | 'paquete';
export type TipoComision = 'porcentaje' | 'monto_fijo' | 'escalonado';
export type TipoBonoComision = 'fijo' | 'porcentaje';
export type EstadoLiquidacion = 'borrador' | 'pagada' | 'cancelada';
export interface EscalonComision {
desde: number;
hasta: number | null;
porcentaje: number;
}
export interface ComisionRegla {
id: number;
name: string;
activo: boolean;
prioridad: number;
empleado_id: number | null;
empleado: string;
staff_type: '' | 'doctor' | 'cosmetologa';
alcance: AlcanceComision;
alcance_label: string;
categoria_code: string;
servicio_id: number | null;
servicio: string;
item_id: number | null;
item: string;
plan_id: number | null;
plan: string;
tipo: TipoComision;
tipo_label: string;
valor: number;
escalones: EscalonComision[];
base_override: '' | 'cobrado' | 'vendido';
fecha_desde: string | null;
fecha_hasta: string | null;
notas: string;
resumen: string;
}
export interface ComisionMeta {
id: number;
empleado_id: number;
empleado: string;
periodo: string;
monto_objetivo: number;
bono: number;
tipo_bono: TipoBonoComision;
activo: boolean;
notas: string;
}
export interface ComisionEmpleado {
id: number;
name: string;
staff_type: 'doctor' | 'cosmetologa';
job_title: string;
commission_pct: number;
reglas_count: number;
metas_count: number;
}
export interface ComisionDesgloseLinea {
venta_id: number;
folio: string;
fecha: string | null;
linea_id: number;
servicio: string;
categoria: string;
tipo_linea: string;
base: number;
regla_id: number | null;
regla: string;
tipo: TipoComision;
porcentaje: number | null;
comision: number;
referencia: string;
}
export interface ComisionPorRegla {
regla_id: number | null;
regla: string;
base: number;
comision: number;
lineas: number;
}
export interface ComisionMetaEval {
meta_id: number;
periodo: string;
monto_objetivo: number;
alcanzada: boolean;
bono: number;
tipo_bono: TipoBonoComision;
bono_config: number;
}
export interface ComisionAjuste {
id: number;
liquidacion_id: number | null;
empleado_id: number;
empleado: string;
periodo: string;
monto: number;
motivo: string;
usuario: string;
fecha: string | null;
}
export interface ComisionEmpleadoCalc {
empleado_id: number;
empleado: string;
staff_type: 'doctor' | 'cosmetologa';
job_title: string;
commission_pct: number;
citas_done: number;
lineas_count: number;
base: number;
comision: number;
bonos: number;
ajustes: number;
total: number;
pagable: boolean;
desglose: ComisionDesgloseLinea[];
por_regla: ComisionPorRegla[];
metas: ComisionMetaEval[];
ajustes_detalle: ComisionAjuste[];
}
export interface ComisionCalculo {
status: string;
start: string;
end: string;
config: ComisionesSettings;
empleados: ComisionEmpleadoCalc[];
totales: { base: number; comision: number; bonos: number; ajustes: number; total: number };
medico_sin_employee?: boolean;
message?: string;
}
export interface ComisionReporteReglas extends ComisionCalculo {
por_servicio: { servicio: string; base: number; comision: number; lineas: number }[];
por_categoria: { categoria: string; base: number; comision: number; lineas: number }[];
}
export interface ComisionLiquidacion {
id: number;
folio: string;
empleado_id: number;
empleado: string;
staff_type: 'doctor' | 'cosmetologa';
periodo_inicio: string | null;
periodo_fin: string | null;
periodo: string;
total_calculado: number;
total_ajustes: number;
total: number;
estado: EstadoLiquidacion;
estado_label: string;
fecha_pago: string | null;
metodo_pago: string;
referencia: string;
notas: string;
creado_por_login: string;
pagada_por_login: string;
ajustes: ComisionAjuste[];
detalle?: ComisionEmpleadoCalc;
}
export type EstadoCompra = 'borrador' | 'enviada' | 'recibida_parcial' | 'recibida' | 'cancelada';
export type MonedaCompra = 'MXN' | 'USD';
export type MetodoPagoCompra = 'efectivo' | 'transferencia' | 'tarjeta' | 'otro';
@@ -1875,6 +2081,7 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
membresias: MEMBRESIAS_DEFAULT,
recursos: RECURSOS_DEFAULT,
compras: COMPRAS_DEFAULT,
comisiones: COMISIONES_DEFAULT,
};
export interface FrontendUser {
@@ -2659,6 +2866,113 @@ export const odooApi = {
return data;
},
// ---- Comisiones por reglas y liquidación ----
// Config (reglas y metas) disponible aunque el motor esté apagado; las rutas
// operativas (cálculo, liquidaciones) responden 403 con `comisiones.activo=false`.
async getComisionesReglas(params?: { empleado_id?: number; alcance?: string; activo?: boolean; q?: string }): Promise<{ status: string; reglas: ComisionRegla[]; total: number; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/reglas`, {
params: {
...(params?.empleado_id ? { empleado_id: params.empleado_id } : {}),
...(params?.alcance ? { alcance: params.alcance } : {}),
...(params?.activo ? { activo: 1 } : {}),
...(params?.q ? { q: params.q } : {}),
},
});
return data;
},
async createComisionRegla(payload: Partial<ComisionRegla>): Promise<{ status: string; regla: ComisionRegla; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/reglas`, payload);
return data;
},
async updateComisionRegla(id: number, payload: Partial<ComisionRegla>): Promise<{ status: string; regla: ComisionRegla; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/comisiones/reglas/${id}`, payload);
return data;
},
async deleteComisionRegla(id: number): Promise<{ status: string; message?: string }> {
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/comisiones/reglas/${id}`);
return data;
},
async getComisionesMetas(params?: { empleado_id?: number; activo?: boolean }): Promise<{ status: string; metas: ComisionMeta[]; total: number; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/metas`, {
params: {
...(params?.empleado_id ? { empleado_id: params.empleado_id } : {}),
...(params?.activo ? { activo: 1 } : {}),
},
});
return data;
},
async createComisionMeta(payload: Partial<ComisionMeta>): Promise<{ status: string; meta: ComisionMeta; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/metas`, payload);
return data;
},
async updateComisionMeta(id: number, payload: Partial<ComisionMeta>): Promise<{ status: string; meta: ComisionMeta; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/comisiones/metas/${id}`, payload);
return data;
},
async deleteComisionMeta(id: number): Promise<{ status: string; message?: string }> {
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/comisiones/metas/${id}`);
return data;
},
async getComisionEmpleados(): Promise<{ status: string; empleados: ComisionEmpleado[]; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/empleados`);
return data;
},
async getComisionCalculo(start: string, end: string, empleadoId?: number): Promise<ComisionCalculo> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/calculo`, {
params: { start, end, ...(empleadoId ? { empleado_id: empleadoId } : {}) },
});
return data;
},
async getComisionesReglasReport(start: string, end: string): Promise<ComisionReporteReglas> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/comisiones_reglas`, { params: { start, end } });
return data;
},
async getComisionesLiquidaciones(params?: { empleado_id?: number; periodo?: string; estado?: EstadoLiquidacion }): Promise<{ status: string; liquidaciones: ComisionLiquidacion[]; total: number; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones`, { params });
return data;
},
async getComisionLiquidacion(id: number): Promise<{ status: string; liquidacion: ComisionLiquidacion; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones/${id}`);
return data;
},
async getMisLiquidaciones(): Promise<{ status: string; empleado_id?: number; empleado?: string; liquidaciones: ComisionLiquidacion[]; medico_sin_employee?: boolean; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/comisiones/mis-liquidaciones`);
return data;
},
async generarComisionesLiquidaciones(payload: { start: string; end: string; empleado_id?: number; empleado_ids?: number[]; notas?: string }): Promise<{ status: string; liquidaciones: ComisionLiquidacion[]; omitidas: { empleado_id: number; empleado: string; folio: string; motivo: string }[]; total_creadas: number; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones`, payload);
return data;
},
async ajustarComisionLiquidacion(id: number, payload: { monto: number; motivo?: string }): Promise<{ status: string; liquidacion: ComisionLiquidacion; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones/${id}/ajustar`, payload);
return data;
},
async pagarComisionLiquidacion(id: number, payload: { fecha?: string; metodo_pago?: string; referencia?: string }): Promise<{ status: string; liquidacion: ComisionLiquidacion; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones/${id}/pagar`, payload);
return data;
},
async cancelarComisionLiquidacion(id: number, motivo?: string): Promise<{ status: string; liquidacion: ComisionLiquidacion; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/comisiones/liquidaciones/${id}/cancelar`, { motivo });
return data;
},
// ---- Membresías y suscripciones ----
// Con `membresias.activo=false` (default) el backend responde 403 y estas
// llamadas nunca se hacen desde la UI (la sección no se monta).