fase 3.2: compras y proveedores (modulo skeen_compras: proveedores, ordenes de compra con recepcion total/parcial, costo promedio ponderado con lote y caducidad, pagos a proveedor, cuentas por pagar con antiguedad, reportes de compras y margen por articulo); apagado por defecto

This commit is contained in:
2026-10-11 12:17:23 +00:00
parent ee8a844d96
commit a916753ebe
38 changed files with 4201 additions and 30 deletions

View File

@@ -816,6 +816,11 @@ export interface InventoryItem {
qty_min: number;
cost: number;
price?: number;
/** Costo promedio ponderado de compras y último costo recibido (módulo Compras). */
costo_promedio?: number;
ultimo_costo?: number;
margen?: number;
margen_pct?: number;
inventory_value: number;
expiry_date: string | null;
last_count_date: string | null;
@@ -1069,6 +1074,7 @@ export interface ClinicSettings {
notificaciones: NotificacionesSettings;
membresias: MembresiasSettings;
recursos: RecursosSettings;
compras: ComprasSettings;
}
// ---- Reservas en línea (portal público de auto-agendado) ----
@@ -1234,6 +1240,238 @@ export const MEMBRESIAS_DEFAULT: MembresiasSettings = {
dias_gracia: 5,
};
// ---- Compras y proveedores ----
export interface ComprasSettings {
/** Enciende TODO el módulo: menú, recepciones, costo promedio y reportes. */
activo: boolean;
/** Al recibir, recalcula el costo promedio ponderado del artículo. */
actualizar_costo_promedio: boolean;
/** Permite recibir menos de lo pedido y completar en recepciones posteriores. */
permitir_recepcion_parcial: boolean;
/** Exige enviar (autorizar) la orden antes de poder recibirla. */
requiere_autorizacion: boolean;
/** Días de crédito por defecto de un proveedor nuevo. */
dias_credito_default: number;
/** Margen (%) bajo el cual el reporte de margen avisa. */
margen_minimo_alerta: number;
/** Permite registrar una recepción sin orden de compra. */
permitir_compras_sin_orden: boolean;
}
export const COMPRAS_DEFAULT: ComprasSettings = {
activo: false,
actualizar_costo_promedio: true,
permitir_recepcion_parcial: true,
requiere_autorizacion: false,
dias_credito_default: 0,
margen_minimo_alerta: 0,
permitir_compras_sin_orden: false,
};
export type EstadoCompra = 'borrador' | 'enviada' | 'recibida_parcial' | 'recibida' | 'cancelada';
export type MonedaCompra = 'MXN' | 'USD';
export type MetodoPagoCompra = 'efectivo' | 'transferencia' | 'tarjeta' | 'otro';
export interface Proveedor {
id: number;
name: string;
email: string;
phone: string;
mobile: string;
vat: string;
street: string;
city: string;
supplier_rank: number;
activo: boolean;
dias_credito: number;
contacto: string;
banco: string;
cuenta_pago: string;
notas: string;
compras_count: number;
saldo: number;
}
export interface CompraLinea {
id: number;
item_id: number;
item: string;
sku: string;
unit: string;
kind: 'producto' | 'consumible';
cantidad_pedida: number;
cantidad_recibida: number;
cantidad_pendiente: number;
costo_unitario: number;
descuento: number;
subtotal: number;
impuesto: number;
total: number;
lote: string;
fecha_caducidad: string | null;
notas: string;
}
export interface CompraRecepcionLinea {
id: number;
item_id: number;
item: string;
cantidad: number;
costo_unitario: number;
lote: string;
fecha_caducidad: string | null;
move_id: number | null;
}
export interface CompraRecepcion {
id: number;
fecha: string | null;
usuario: string;
notas: string;
total_unidades: number;
total_costo: number;
lineas: CompraRecepcionLinea[];
}
export interface CompraPago {
id: number;
fecha: string | null;
monto: number;
moneda: MonedaCompra;
metodo: MetodoPagoCompra;
metodo_label: string;
referencia: string;
notas: string;
usuario: string;
monto_en_compra: number;
}
export interface Compra {
id: number;
folio: string;
partner_id: number;
proveedor: string;
fecha: string | null;
fecha_entrega_esperada: string | null;
estado: EstadoCompra;
estado_label: string;
moneda: MonedaCompra;
subtotal: number;
descuento: number;
impuestos: number;
total: number;
pagado: number;
saldo: number;
sucursal_id: number | null;
sucursal: string;
referencia_factura: string;
notas: string;
creado_por_login: string;
recibida_at: string | null;
cancel_reason: string;
lineas_count: number;
recepciones_count: number;
tiene_recepciones: boolean;
total_pedido: number;
total_recibido: number;
puede_recibir: boolean;
puede_cancelar: boolean;
lineas?: CompraLinea[];
recepciones?: CompraRecepcion[];
pagos?: CompraPago[];
}
export interface CompraInput {
partner_id?: number;
fecha?: string;
fecha_entrega_esperada?: string | null;
moneda?: MonedaCompra;
descuento?: number;
notas?: string;
referencia_factura?: string;
sucursal_id?: number | null;
lineas?: {
item_id: number;
cantidad_pedida: number;
costo_unitario?: number;
descuento?: number;
lote?: string;
fecha_caducidad?: string | null;
notas?: string;
}[];
}
export interface CompraResumen {
abiertas: number;
por_pagar: number;
saldo_total: number;
compras_mes: number;
gasto_mes: number;
borradores: number;
enviadas: number;
}
export interface CuentaPorPagar {
partner_id: number;
proveedor: string;
dias_credito: number;
saldo: number;
compras: number;
buckets: Record<string, number>;
detalle: {
id: number; folio: string; fecha: string | null; fecha_vencimiento: string | null;
dias: number; bucket: string; total: number; pagado: number; saldo: number;
moneda: MonedaCompra;
}[];
}
export interface CuentasPorPagarResponse {
status: string;
proveedores: CuentaPorPagar[];
buckets: { key: string; label: string }[];
totales: { saldo: number; por_bucket: Record<string, number>; proveedores: number; compras: number };
}
export interface ComprasReportResponse {
status: string;
start: string;
end: string;
totales: { compras: number; subtotal: number; impuestos: number; total: number; pagado: number; saldo: number };
por_proveedor: { partner_id: number; proveedor: string; compras: number; total: number; pagado: number; saldo: number }[];
por_articulo: { item_id: number; item: string; sku: string; category: string; unidad: string;
cantidad_pedida: number; cantidad_recibida: number; subtotal: number }[];
por_mes: { mes: string; total: number }[];
cuentas_por_pagar: { total: number; proveedores: { partner_id: number; proveedor: string; saldo: number; compras: number; dias_credito: number }[] };
message?: string;
}
export interface MargenReportFila {
item_id: number;
item: string;
sku: string;
category: string;
unidad: string;
precio: number;
costo_promedio: number;
margen_unitario: number;
margen_pct: number;
unidades_vendidas: number;
ingresos: number;
costo_ventas: number;
margen_total: number;
alerta: boolean;
}
export interface MargenReportResponse {
status: string;
start: string;
end: string;
margen_minimo_alerta: number;
articulos: MargenReportFila[];
totales: { ingresos: number; costo_ventas: number; margen: number; margen_pct: number; alertas: number };
message?: string;
}
export type PeriodicidadMembresia = 'semanal' | 'mensual' | 'trimestral' | 'semestral' | 'anual';
export type EstadoMembresia = 'activa' | 'congelada' | 'vencida' | 'pausada' | 'cancelada';
export type EstadoPeriodoMembresia = 'pendiente' | 'pagado' | 'vencido' | 'condonado';
@@ -1636,6 +1874,7 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
notificaciones: NOTIFICACIONES_DEFAULT,
membresias: MEMBRESIAS_DEFAULT,
recursos: RECURSOS_DEFAULT,
compras: COMPRAS_DEFAULT,
};
export interface FrontendUser {
@@ -2330,6 +2569,96 @@ export const odooApi = {
return data;
},
// ---- Compras y proveedores ----
// Con `compras.activo=false` (default) el backend responde 403 y estas
// llamadas nunca se hacen desde la UI (la sección no se monta).
async getProveedores(opts?: { includeInactive?: boolean; q?: string; page?: number; page_size?: number }): Promise<{
status: string; proveedores: Proveedor[]; total: number; page: number; page_size: number; message?: string;
}> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/proveedores`, {
params: {
...(opts?.includeInactive ? { include_inactive: 1 } : {}),
...(opts?.q ? { q: opts.q } : {}),
...(opts?.page ? { page: opts.page } : {}),
...(opts?.page_size ? { page_size: opts.page_size } : {}),
},
});
return data;
},
async createProveedor(payload: Partial<Proveedor> & { name: string }): Promise<{ status: string; proveedor: Proveedor; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/proveedores`, payload);
return data;
},
async updateProveedor(id: number, payload: Partial<Proveedor>): Promise<{ status: string; proveedor: Proveedor; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/proveedores/${id}`, payload);
return data;
},
async deleteProveedor(id: number): Promise<{ status: string; proveedor?: Proveedor; message?: string; desactivar?: boolean }> {
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/proveedores/${id}`);
return data;
},
async getCompras(params?: { estado?: string; partner_id?: number; start?: string; end?: string; q?: string; page?: number; page_size?: number }): Promise<{
status: string; compras: Compra[]; total: number; page: number; page_size: number; total_pages: number;
resumen: CompraResumen; message?: string;
}> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/compras`, { params });
return data;
},
async getCompra(id: number): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}`);
return data;
},
async crearOrdenCompra(payload: CompraInput): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/compras`, payload);
return data;
},
async updateCompra(id: number, payload: CompraInput): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}`, payload);
return data;
},
async enviarCompra(id: number): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}/enviar`, {});
return data;
},
async recibirCompra(id: number, payload: { notas?: string; lineas: { linea_id: number; cantidad: number; lote?: string; fecha_caducidad?: string | null; costo_unitario?: number }[] }): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}/recibir`, payload);
return data;
},
async cancelarCompra(id: number, motivo?: string): Promise<{ status: string; compra: Compra; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}/cancelar`, { motivo });
return data;
},
async pagarCompra(id: number, payload: { monto: number; moneda?: MonedaCompra; metodo?: MetodoPagoCompra; referencia?: string; notas?: string }): Promise<{ status: string; compra: Compra; pago: CompraPago; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/compras/${id}/pagar`, payload);
return data;
},
async getCuentasPorPagar(params?: { partner_id?: number }): Promise<CuentasPorPagarResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/compras/cuentas_por_pagar`, { params });
return data;
},
async getComprasReport(start: string, end: string): Promise<ComprasReportResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/compras`, { params: { start, end } });
return data;
},
async getMargenReport(start: string, end: string): Promise<MargenReportResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/margen`, { params: { start, end } });
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).