-- 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;