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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user