De planillas diarias a stock sincronizado cada 15 minutos
Dos personas del área comercial dedicaban la primera hora de cada día a exportar stock desde Softland y subirlo a una planilla compartida.
-- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico); -- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico);
// worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; } // worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; }
// conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC; // conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC;
Tres integraciones sobre Softland que llevan meses o años funcionando. Los nombres van reservados por acuerdo con cada cliente, las cifras no.
Dos personas del área comercial dedicaban la primera hora de cada día a exportar stock desde Softland y subirlo a una planilla compartida.
Los clientes mayoristas pedían por WhatsApp y correo.
Los informes de gestión los armaba una persona en Excel a partir de exportaciones de Softland y llegaban entre el diez y el quince del mes siguiente.
-- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico); -- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico);
// worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; } // worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; }
// conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC; // conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC;
-- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico); -- sincronización de stock por bodega CREATE PROCEDURE sp_sync_stock @desde datetime AS BEGIN SET NOCOUNT ON; SELECT a.cod_prod, b.cod_bodega, SUM(b.stock_fisico) AS stock, GETDATE() AS sincronizado FROM mae_producto a JOIN mov_bodega b ON b.cod_prod = a.cod_prod WHERE a.activo = 'S' AND b.fecha_mov >= @desde GROUP BY a.cod_prod, b.cod_bodega HAVING SUM(b.stock_fisico) > 0; -- bitácora con reintento INSERT INTO backoffice_sync_log (entidad, referencia, estado, intento, ts) VALUES ('stock', @desde, 'OK', 1, GETDATE()); END GO -- índice para no bloquear en horario laboral CREATE NONCLUSTERED INDEX ix_mov_bodega_fecha ON mov_bodega (fecha_mov) INCLUDE (cod_prod, stock_fisico);
// worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; } // worker de sincronización Softland → web import { sql } from './mssql'; import { db } from './postgres'; type SyncResult = { entidad: string; registros: number; ms: number; }; export async function syncStock(): Promise<SyncResult> { const t0 = performance.now(); const filas = await sql.query('EXEC sp_sync_stock @desde', { desde: lastRun('stock'), }); await db.transaction(async (trx) => { for (const f of filas) { await trx('productos') .insert({ sku: f.cod_prod, stock: f.stock }) .onConflict('sku') .merge(); } }); return { entidad: 'stock', registros: filas.length, ms: Math.round(performance.now() - t0), }; }
// conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC; // conciliación de cartolas bancarias export async function conciliar(cartola: Movimiento[]) { const pendientes = await cuentasPorCobrar(); for (const mov of cartola) { const calce = pendientes.find((c) => c.monto === mov.abono && c.rut === mov.rut); if (calce) { await aplicarPago(calce.documento, mov.fecha); } else { await dejarPendiente(mov, 'sin calce automático'); } } } -- movimientos sin conciliar SELECT fecha, glosa, abono, rut_cliente FROM banco_movimiento WHERE conciliado = 'N' ORDER BY fecha DESC;
Una reunión de 30 minutos. Revisamos tu requerimiento y te decimos si tiene solución, cuánto demora y cuánto cuesta. Si no podemos ayudarte, también te lo decimos.
Respondemos en menos de 24 horas hábiles.
Gracias. Te respondemos a tu correo en menos de 24 horas hábiles.