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