Initial commit : API Node.js + PostgreSQL + sync service Docker
- API Express/pg connectée à PostgreSQL (converti depuis mssql) - 8 routes : articles, fournisseurs, commandes, stock, mouvements, performance, schema, sync - Service sync nightly (mssql → pg, cron 3h00) - Docker Compose : postgres + api + sync + frpc (tunnel sortant) - Schéma PostgreSQL 17 tables dans postgres/init.sql Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
d565ed9f10
28 files changed
+2423
No files matched your search
@@ -0,0 +1,6 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
CMD ["node", "index.js"]
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
require('dotenv').config();
|
||||
const sql = require('mssql');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const mssqlConfig = {
|
||||
server: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT) || 1433,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true,
|
||||
readOnlyIntent: true,
|
||||
},
|
||||
pool: { max: 5, min: 0, idleTimeoutMillis: 30000 },
|
||||
};
|
||||
|
||||
let mssqlPool = null;
|
||||
let pgPool = null;
|
||||
|
||||
async function getMssql() {
|
||||
if (!mssqlPool) {
|
||||
mssqlPool = await sql.connect(mssqlConfig);
|
||||
}
|
||||
return mssqlPool;
|
||||
}
|
||||
|
||||
function getPg() {
|
||||
if (!pgPool) {
|
||||
pgPool = new Pool({
|
||||
host: process.env.PG_HOST || 'postgres',
|
||||
port: parseInt(process.env.PG_PORT) || 5432,
|
||||
database: process.env.PG_DB,
|
||||
user: process.env.PG_USER,
|
||||
password: process.env.PG_PASSWORD,
|
||||
max: 5,
|
||||
});
|
||||
}
|
||||
return pgPool;
|
||||
}
|
||||
|
||||
async function closeAll() {
|
||||
if (mssqlPool) { await mssqlPool.close(); mssqlPool = null; }
|
||||
if (pgPool) { await pgPool.end(); pgPool = null; }
|
||||
}
|
||||
|
||||
module.exports = { getMssql, getPg, closeAll, sql };
|
||||
@@ -0,0 +1,46 @@
|
||||
require('dotenv').config();
|
||||
const cron = require('node-cron');
|
||||
|
||||
const { syncArticles } = require('./tables/articles');
|
||||
const { syncFournisseurs } = require('./tables/fournisseurs');
|
||||
const { syncStock } = require('./tables/stock');
|
||||
const { syncMouvements } = require('./tables/mouvements');
|
||||
const { syncCommandes } = require('./tables/commandes');
|
||||
const { syncReferentiel } = require('./tables/referentiel');
|
||||
const { closeAll } = require('./db');
|
||||
|
||||
const forceMode = process.argv.includes('--force');
|
||||
|
||||
async function syncAll(force = false) {
|
||||
const start = Date.now();
|
||||
console.log(`\n=== SYNC START ${new Date().toISOString()} (force=${force}) ===`);
|
||||
|
||||
// Ordre important : référentiel avant articles, articles avant stock/mouvements
|
||||
await syncReferentiel(force);
|
||||
await syncArticles(force);
|
||||
await syncFournisseurs(force);
|
||||
await syncStock(force);
|
||||
await syncMouvements(force);
|
||||
await syncCommandes(force);
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`=== SYNC DONE en ${elapsed}s ===\n`);
|
||||
}
|
||||
|
||||
if (forceMode) {
|
||||
// Lancement manuel : node index.js --force
|
||||
console.log('Mode force : sync complète en cours...');
|
||||
syncAll(true)
|
||||
.then(() => closeAll())
|
||||
.then(() => process.exit(0))
|
||||
.catch(err => {
|
||||
console.error('Erreur fatale sync:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
// Mode daemon : sync nightly à 3h00
|
||||
console.log('Service sync démarré — cron: 0 3 * * * (3h00 chaque nuit)');
|
||||
cron.schedule('0 3 * * *', () => {
|
||||
syncAll(false).catch(err => console.error('Erreur cron sync:', err.message));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "foirfouille-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Sync nightly SQL Server → PostgreSQL",
|
||||
"main": "index.js",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"mssql": "^12.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"pg": "^8.13.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { batchUpsert, getLastSync, logSync } = require('../utils');
|
||||
|
||||
const ARTICLES_COLS = [
|
||||
'no_id','codein','libelle1','libelle2','lib_ticket',
|
||||
'tax_code','ach_code','utilisable','actif','suspendu',
|
||||
'suividatecreation','suividatemodif','nom_no_id',
|
||||
];
|
||||
const INFOSUP_COLS = [
|
||||
'artnoid','prix_vente_mini','prix_vente_maxi','eco_ht','eco_ttc',
|
||||
'on_web','interdit_remise','nomphoto','datedebvente','datefinvente',
|
||||
'datevaliditeachat','commentaire','pv_conseille','fraislogistic',
|
||||
'transit','distribution',
|
||||
];
|
||||
const GTIN_COLS = ['idarticle','gtin','preferentiel'];
|
||||
|
||||
async function syncArticles(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === ARTICLES ===
|
||||
try {
|
||||
const lastSync = force ? null : await getLastSync(pg, 'articles');
|
||||
const where = lastSync ? `WHERE SUIVIDATEMODIF > '${lastSync.toISOString()}'` : '';
|
||||
|
||||
const res = await ms.request().query(`
|
||||
SELECT NO_ID, CODEIN, LIBELLE1, LIBELLE2, LIB_TICKET,
|
||||
TAX_CODE, ACH_CODE, UTILISABLE, ACTIF, SUSPENDU,
|
||||
SUIVIDATECREATION, SUIVIDATEMODIF, NOM_NO_ID
|
||||
FROM ARTICLES ${where}
|
||||
`);
|
||||
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
codein: r.CODEIN,
|
||||
libelle1: r.LIBELLE1,
|
||||
libelle2: r.LIBELLE2,
|
||||
lib_ticket: r.LIB_TICKET,
|
||||
tax_code: r.TAX_CODE,
|
||||
ach_code: r.ACH_CODE,
|
||||
utilisable: r.UTILISABLE,
|
||||
actif: r.ACTIF,
|
||||
suspendu: r.SUSPENDU,
|
||||
suividatecreation: r.SUIVIDATECREATION,
|
||||
suividatemodif: r.SUIVIDATEMODIF,
|
||||
nom_no_id: r.NOM_NO_ID,
|
||||
}));
|
||||
|
||||
const count = await batchUpsert(pg, 'articles', rows, ['no_id'], ARTICLES_COLS);
|
||||
await logSync(pg, 'articles', count, 'ok');
|
||||
console.log(`[articles] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'articles', 0, 'error', err.message);
|
||||
console.error(`[articles] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === ARTICLE_INFOSUP ===
|
||||
try {
|
||||
const lastSync = force ? null : await getLastSync(pg, 'article_infosup');
|
||||
const where = lastSync
|
||||
? `WHERE ai.ARTNOID IN (SELECT NO_ID FROM ARTICLES WHERE SUIVIDATEMODIF > '${lastSync.toISOString()}')`
|
||||
: '';
|
||||
|
||||
const res = await ms.request().query(`
|
||||
SELECT ARTNOID, PRIX_VENTE_MINI, PRIX_VENTE_MAXI, ECO_HT, ECO_TTC,
|
||||
ON_WEB, INTERDIT_REMISE, NOMPHOTO, DATEDEBVENTE, DATEFINVENTE,
|
||||
DATEVALIDITEACHAT, COMMENTAIRE, PV_CONSEILLE, FRAISLOGISTIC,
|
||||
TRANSIT, DISTRIBUTION
|
||||
FROM ARTICLE_INFOSUP ai ${where}
|
||||
`);
|
||||
|
||||
const rows = res.recordset.map(r => ({
|
||||
artnoid: r.ARTNOID,
|
||||
prix_vente_mini: r.PRIX_VENTE_MINI,
|
||||
prix_vente_maxi: r.PRIX_VENTE_MAXI,
|
||||
eco_ht: r.ECO_HT,
|
||||
eco_ttc: r.ECO_TTC,
|
||||
on_web: r.ON_WEB,
|
||||
interdit_remise: r.INTERDIT_REMISE,
|
||||
nomphoto: r.NOMPHOTO,
|
||||
datedebvente: r.DATEDEBVENTE,
|
||||
datefinvente: r.DATEFINVENTE,
|
||||
datevaliditeachat: r.DATEVALIDITEACHAT,
|
||||
commentaire: r.COMMENTAIRE,
|
||||
pv_conseille: r.PV_CONSEILLE,
|
||||
fraislogistic: r.FRAISLOGISTIC,
|
||||
transit: r.TRANSIT,
|
||||
distribution: r.DISTRIBUTION,
|
||||
}));
|
||||
|
||||
const count = await batchUpsert(pg, 'article_infosup', rows, ['artnoid'], INFOSUP_COLS);
|
||||
await logSync(pg, 'article_infosup', count, 'ok');
|
||||
console.log(`[article_infosup] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'article_infosup', 0, 'error', err.message);
|
||||
console.error(`[article_infosup] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === ART_GTIN (full refresh — petite table) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT IDARTICLE, GTIN, PREFERENTIEL FROM ART_GTIN`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
idarticle: r.IDARTICLE,
|
||||
gtin: r.GTIN,
|
||||
preferentiel: r.PREFERENTIEL,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'art_gtin', rows, ['idarticle','gtin'], GTIN_COLS);
|
||||
await logSync(pg, 'art_gtin', count, 'ok');
|
||||
console.log(`[art_gtin] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'art_gtin', 0, 'error', err.message);
|
||||
console.error(`[art_gtin] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncArticles };
|
||||
@@ -0,0 +1,148 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { fullRefresh, batchUpsert, getLastSync, logSync } = require('../utils');
|
||||
|
||||
const VIVANT_COLS = [
|
||||
'cdefou_ligne_com_no_id','artfou1_no_id','articles_codein','articles_libelle1',
|
||||
'articles_libelle2','artfou1_reference','artfou1_ean13','artfou1_itf',
|
||||
'cdefou_ligne_qtecde','cdefou_ligne_prixbrut','cdefou_ligne_remise',
|
||||
'cdefou_ligne_remise2','cdefou_ligne_remise3','cdefou_ligne_prixnet',
|
||||
'cdefou_ligne_montant','cdefou_ligne_prixvente','cdefou_ligne_gratuit',
|
||||
'cdefou_ligne_qteacc','cdefou_ligne_qteann','cdefou_ligne_qteatt',
|
||||
'cdefou_ligne_qterel','cdefou_ligne_cdeligtard','cdefou_ligne_cdeligtot',
|
||||
'cotisation_logistique','fraislogistic','transit','distribution','taxe',
|
||||
'commentaire','suividatecreation',
|
||||
];
|
||||
const RECEP_COLS = ['no_id','suividatecreation','suividatemodif'];
|
||||
const RECEPLIG_COLS = [
|
||||
'no_id','cdefou_reception_no_id','artfou1_no_id',
|
||||
'qtebl','qterec','qteacc','qteref','qteaff',
|
||||
'pribrut','remise','prirec','mntrec',
|
||||
'motifrefus','recpb','recpbok',
|
||||
'suividatecreation','suividatemodif',
|
||||
];
|
||||
|
||||
async function syncCommandes(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === CDEFOU_VIVANT (full refresh — commandes en cours uniquement) ===
|
||||
try {
|
||||
const res = await ms.request().query(`
|
||||
SELECT
|
||||
CDEFOU_LIGNE_COM_NO_ID, ARTFOU1_NO_ID,
|
||||
ARTICLES_CODEIN, ARTICLES_LIBELLE1, ARTICLES_LIBELLE2,
|
||||
ARTFOU1_REFERENCE, ARTFOU1_EAN13, ARTFOU1_ITF,
|
||||
CDEFOU_LIGNE_QTECDE, CDEFOU_LIGNE_PRIXBRUT, CDEFOU_LIGNE_REMISE,
|
||||
CDEFOU_LIGNE_REMISE2, CDEFOU_LIGNE_REMISE3, CDEFOU_LIGNE_PRIXNET,
|
||||
CDEFOU_LIGNE_MONTANT, CDEFOU_LIGNE_PRIXVENTE, CDEFOU_LIGNE_GRATUIT,
|
||||
CDEFOU_LIGNE_QTEACC, CDEFOU_LIGNE_QTEANN, CDEFOU_LIGNE_QTEATT,
|
||||
CDEFOU_LIGNE_QTEREL, CDEFOU_LIGNE_CDELIGTARD, CDEFOU_LIGNE_CDELIGTOT,
|
||||
COTISATION_LOGISTIQUE, FRAISLOGISTIC, TRANSIT, DISTRIBUTION, TAXE,
|
||||
COMMENTAIRE, SUIVIDATECREATION
|
||||
FROM CDEFOU_VIVANT
|
||||
`);
|
||||
|
||||
const rows = res.recordset.map(r => ({
|
||||
cdefou_ligne_com_no_id: r.CDEFOU_LIGNE_COM_NO_ID,
|
||||
artfou1_no_id: r.ARTFOU1_NO_ID,
|
||||
articles_codein: r.ARTICLES_CODEIN,
|
||||
articles_libelle1: r.ARTICLES_LIBELLE1,
|
||||
articles_libelle2: r.ARTICLES_LIBELLE2,
|
||||
artfou1_reference: r.ARTFOU1_REFERENCE,
|
||||
artfou1_ean13: r.ARTFOU1_EAN13,
|
||||
artfou1_itf: r.ARTFOU1_ITF,
|
||||
cdefou_ligne_qtecde: r.CDEFOU_LIGNE_QTECDE,
|
||||
cdefou_ligne_prixbrut: r.CDEFOU_LIGNE_PRIXBRUT,
|
||||
cdefou_ligne_remise: r.CDEFOU_LIGNE_REMISE,
|
||||
cdefou_ligne_remise2: r.CDEFOU_LIGNE_REMISE2,
|
||||
cdefou_ligne_remise3: r.CDEFOU_LIGNE_REMISE3,
|
||||
cdefou_ligne_prixnet: r.CDEFOU_LIGNE_PRIXNET,
|
||||
cdefou_ligne_montant: r.CDEFOU_LIGNE_MONTANT,
|
||||
cdefou_ligne_prixvente: r.CDEFOU_LIGNE_PRIXVENTE,
|
||||
cdefou_ligne_gratuit: r.CDEFOU_LIGNE_GRATUIT,
|
||||
cdefou_ligne_qteacc: r.CDEFOU_LIGNE_QTEACC,
|
||||
cdefou_ligne_qteann: r.CDEFOU_LIGNE_QTEANN,
|
||||
cdefou_ligne_qteatt: r.CDEFOU_LIGNE_QTEATT,
|
||||
cdefou_ligne_qterel: r.CDEFOU_LIGNE_QTEREL,
|
||||
cdefou_ligne_cdeligtard: r.CDEFOU_LIGNE_CDELIGTARD,
|
||||
cdefou_ligne_cdeligtot: r.CDEFOU_LIGNE_CDELIGTOT,
|
||||
cotisation_logistique: r.COTISATION_LOGISTIQUE,
|
||||
fraislogistic: r.FRAISLOGISTIC,
|
||||
transit: r.TRANSIT,
|
||||
distribution: r.DISTRIBUTION,
|
||||
taxe: r.TAXE,
|
||||
commentaire: r.COMMENTAIRE,
|
||||
suividatecreation: r.SUIVIDATECREATION,
|
||||
}));
|
||||
|
||||
const count = await fullRefresh(pg, 'cdefou_vivant', rows, VIVANT_COLS);
|
||||
await logSync(pg, 'cdefou_vivant', count, 'ok');
|
||||
console.log(`[cdefou_vivant] ${count} lignes refresh`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cdefou_vivant', 0, 'error', err.message);
|
||||
console.error(`[cdefou_vivant] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === CDEFOU_RECEPTION (upsert delta) ===
|
||||
try {
|
||||
const lastSync = force ? null : await getLastSync(pg, 'cdefou_reception');
|
||||
const where = lastSync ? `WHERE SUIVIDATEMODIF > '${lastSync.toISOString()}'` : '';
|
||||
|
||||
const res = await ms.request().query(
|
||||
`SELECT NO_ID, SUIVIDATECREATION, SUIVIDATEMODIF FROM CDEFOU_RECEPTION ${where}`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
suividatecreation: r.SUIVIDATECREATION,
|
||||
suividatemodif: r.SUIVIDATEMODIF,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'cdefou_reception', rows, ['no_id'], RECEP_COLS);
|
||||
await logSync(pg, 'cdefou_reception', count, 'ok');
|
||||
console.log(`[cdefou_reception] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cdefou_reception', 0, 'error', err.message);
|
||||
console.error(`[cdefou_reception] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === CDEFOU_RECEPLIG (upsert delta) ===
|
||||
try {
|
||||
const lastSync = force ? null : await getLastSync(pg, 'cdefou_receplig');
|
||||
const where = lastSync ? `WHERE SUIVIDATEMODIF > '${lastSync.toISOString()}'` : '';
|
||||
|
||||
const res = await ms.request().query(`
|
||||
SELECT NO_ID, CDEFOU_RECEPTION_NO_ID, ARTFOU1_NO_ID,
|
||||
QTEBL, QTEREC, QTEACC, QTEREF, QTEAFF,
|
||||
PRIBRUT, REMISE, PRIREC, MNTREC,
|
||||
MOTIFREFUS, RECPB, RECPBOK,
|
||||
SUIVIDATECREATION, SUIVIDATEMODIF
|
||||
FROM CDEFOU_RECEPLIG ${where}
|
||||
`);
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
cdefou_reception_no_id: r.CDEFOU_RECEPTION_NO_ID,
|
||||
artfou1_no_id: r.ARTFOU1_NO_ID,
|
||||
qtebl: r.QTEBL,
|
||||
qterec: r.QTEREC,
|
||||
qteacc: r.QTEACC,
|
||||
qteref: r.QTEREF,
|
||||
qteaff: r.QTEAFF,
|
||||
pribrut: r.PRIBRUT,
|
||||
remise: r.REMISE,
|
||||
prirec: r.PRIREC,
|
||||
mntrec: r.MNTREC,
|
||||
motifrefus: r.MOTIFREFUS,
|
||||
recpb: r.RECPB,
|
||||
recpbok: r.RECPBOK,
|
||||
suividatecreation: r.SUIVIDATECREATION,
|
||||
suividatemodif: r.SUIVIDATEMODIF,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'cdefou_receplig', rows, ['no_id'], RECEPLIG_COLS);
|
||||
await logSync(pg, 'cdefou_receplig', count, 'ok');
|
||||
console.log(`[cdefou_receplig] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cdefou_receplig', 0, 'error', err.message);
|
||||
console.error(`[cdefou_receplig] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncCommandes };
|
||||
@@ -0,0 +1,94 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { batchUpsert, fullRefresh, getLastSync, logSync } = require('../utils');
|
||||
|
||||
const ARTFOU1_COLS = [
|
||||
'no_id','art_no_id','code','reference','ean13','itf',
|
||||
'qteua','pcb','spcb','delai','securite','preference','suspendu',
|
||||
'suividatecreation','suividatemodif',
|
||||
];
|
||||
const ARTFOU2_COLS = ['idartfou1','prixachat','remise_promotion'];
|
||||
const FOUADR1_COLS = ['code','sit_code','raisonsociale','adrligne1','telephone','email'];
|
||||
|
||||
async function syncFournisseurs(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === ARTFOU1 (delta par SUIVIDATEMODIF) ===
|
||||
try {
|
||||
const lastSync = force ? null : await getLastSync(pg, 'artfou1');
|
||||
const where = lastSync ? `WHERE SUIVIDATEMODIF > '${lastSync.toISOString()}'` : '';
|
||||
|
||||
const res = await ms.request().query(`
|
||||
SELECT NO_ID, ART_NO_ID, CODE, REFERENCE, EAN13, ITF,
|
||||
QTEUA, PCB, SPCB, DELAI, SECURITE, PREFERENCE, SUSPENDU,
|
||||
SUIVIDATECREATION, SUIVIDATEMODIF
|
||||
FROM ARTFOU1 ${where}
|
||||
`);
|
||||
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
art_no_id: r.ART_NO_ID,
|
||||
code: r.CODE,
|
||||
reference: r.REFERENCE,
|
||||
ean13: r.EAN13,
|
||||
itf: r.ITF,
|
||||
qteua: r.QTEUA,
|
||||
pcb: r.PCB,
|
||||
spcb: r.SPCB,
|
||||
delai: r.DELAI,
|
||||
securite: r.SECURITE,
|
||||
preference: r.PREFERENCE,
|
||||
suspendu: r.SUSPENDU,
|
||||
suividatecreation: r.SUIVIDATECREATION,
|
||||
suividatemodif: r.SUIVIDATEMODIF,
|
||||
}));
|
||||
|
||||
const count = await batchUpsert(pg, 'artfou1', rows, ['no_id'], ARTFOU1_COLS);
|
||||
await logSync(pg, 'artfou1', count, 'ok');
|
||||
console.log(`[artfou1] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'artfou1', 0, 'error', err.message);
|
||||
console.error(`[artfou1] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === ARTFOU2 (full refresh — liée à ARTFOU1) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT IDARTFOU1, PRIXACHAT, REMISE_PROMOTION FROM ARTFOU2`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
idartfou1: r.IDARTFOU1,
|
||||
prixachat: r.PRIXACHAT,
|
||||
remise_promotion: r.REMISE_PROMOTION,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'artfou2', rows, ['idartfou1'], ARTFOU2_COLS);
|
||||
await logSync(pg, 'artfou2', count, 'ok');
|
||||
console.log(`[artfou2] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'artfou2', 0, 'error', err.message);
|
||||
console.error(`[artfou2] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === FOUADR1 (full refresh — ~500 lignes) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT CODE, SIT_CODE, RAISONSOCIALE, ADRLIGNE1, TELEPHONE, EMAIL FROM FOUADR1`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
code: r.CODE,
|
||||
sit_code: r.SIT_CODE,
|
||||
raisonsociale: r.RAISONSOCIALE,
|
||||
adrligne1: r.ADRLIGNE1,
|
||||
telephone: r.TELEPHONE,
|
||||
email: r.EMAIL,
|
||||
}));
|
||||
const count = await fullRefresh(pg, 'fouadr1', rows, FOUADR1_COLS);
|
||||
await logSync(pg, 'fouadr1', count, 'ok');
|
||||
console.log(`[fouadr1] ${count} lignes refresh`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'fouadr1', 0, 'error', err.message);
|
||||
console.error(`[fouadr1] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncFournisseurs };
|
||||
@@ -0,0 +1,138 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { logSync } = require('../utils');
|
||||
|
||||
const MVTART_COLS = [
|
||||
'artnoid','datmvt','site','libmvt','genremvt',
|
||||
'qtemvt','valmvt','mntmvtht','mntmvtttc','margemvt',
|
||||
'qtestock','prmp','valstock','codefou',
|
||||
];
|
||||
const MVTREG_COLS = [
|
||||
'datmvt','codtick','codcartecli','coddev',
|
||||
'mntreg','mntregdev','clientnom','echeance',
|
||||
'reference','typereg','suividatecreation',
|
||||
];
|
||||
|
||||
const CHUNK = 2000;
|
||||
|
||||
async function insertChunk(pg, table, rows, cols) {
|
||||
if (!rows.length) return;
|
||||
const values = [];
|
||||
const placeholders = rows.map((row, ri) => {
|
||||
const ph = cols.map((col, ci) => {
|
||||
values.push(row[col] ?? null);
|
||||
return `$${ri * cols.length + ci + 1}`;
|
||||
});
|
||||
return `(${ph.join(', ')})`;
|
||||
});
|
||||
await pg.query(
|
||||
`INSERT INTO ${table} (${cols.join(', ')}) VALUES ${placeholders.join(', ')}`,
|
||||
values
|
||||
);
|
||||
}
|
||||
|
||||
async function syncMouvements(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === MvtArt (insert-only par date croissante) ===
|
||||
try {
|
||||
// Date pivot : max existant en pg, ou 2 ans en arrière pour sync initiale
|
||||
const maxRes = await pg.query(`SELECT MAX(datmvt) AS last FROM mvtart`);
|
||||
let lastDate = maxRes.rows[0]?.last;
|
||||
if (!lastDate || force) {
|
||||
const twoYearsAgo = new Date();
|
||||
twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
|
||||
lastDate = twoYearsAgo;
|
||||
}
|
||||
|
||||
const res = await ms.request()
|
||||
.input('lastDate', lastDate)
|
||||
.query(`
|
||||
SELECT ArtNoId, DatMvt, Site, LibMvt, GenreMvt,
|
||||
QteMvt, ValMvt, MntMvtHt, MntMvtTTC, MargeMvt,
|
||||
QteStock, Prmp, ValStock, CODEFOU
|
||||
FROM MvtArt
|
||||
WHERE DatMvt > @lastDate
|
||||
ORDER BY DatMvt
|
||||
`);
|
||||
|
||||
const allRows = res.recordset.map(r => ({
|
||||
artnoid: r.ArtNoId,
|
||||
datmvt: r.DatMvt,
|
||||
site: r.Site,
|
||||
libmvt: r.LibMvt,
|
||||
genremvt: r.GenreMvt,
|
||||
qtemvt: r.QteMvt,
|
||||
valmvt: r.ValMvt,
|
||||
mntmvtht: r.MntMvtHt,
|
||||
mntmvtttc: r.MntMvtTTC,
|
||||
margemvt: r.MargeMvt,
|
||||
qtestock: r.QteStock,
|
||||
prmp: r.Prmp,
|
||||
valstock: r.ValStock,
|
||||
codefou: r.CODEFOU,
|
||||
}));
|
||||
|
||||
let inserted = 0;
|
||||
for (let i = 0; i < allRows.length; i += CHUNK) {
|
||||
await insertChunk(pg, 'mvtart', allRows.slice(i, i + CHUNK), MVTART_COLS);
|
||||
inserted += Math.min(CHUNK, allRows.length - i);
|
||||
}
|
||||
|
||||
await logSync(pg, 'mvtart', inserted, 'ok');
|
||||
console.log(`[mvtart] ${inserted} nouvelles lignes`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'mvtart', 0, 'error', err.message);
|
||||
console.error(`[mvtart] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === MvtReg (insert-only par date) ===
|
||||
try {
|
||||
const maxRes = await pg.query(`SELECT MAX(datmvt) AS last FROM mvtreg`);
|
||||
let lastDate = maxRes.rows[0]?.last;
|
||||
if (!lastDate || force) {
|
||||
const twoYearsAgo = new Date();
|
||||
twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
|
||||
lastDate = twoYearsAgo;
|
||||
}
|
||||
|
||||
const res = await ms.request()
|
||||
.input('lastDate', lastDate)
|
||||
.query(`
|
||||
SELECT DatMvt, CodTick, CodCarteCli, CodDev,
|
||||
MntReg, MntRegDev, ClientNom, Echeance,
|
||||
REFERENCE, TYPEREG, SuiviDateCreation
|
||||
FROM MvtReg
|
||||
WHERE DatMvt > @lastDate
|
||||
ORDER BY DatMvt
|
||||
`);
|
||||
|
||||
const allRows = res.recordset.map(r => ({
|
||||
datmvt: r.DatMvt,
|
||||
codtick: r.CodTick,
|
||||
codcartecli: r.CodCarteCli,
|
||||
coddev: r.CodDev,
|
||||
mntreg: r.MntReg,
|
||||
mntregdev: r.MntRegDev,
|
||||
clientnom: r.ClientNom,
|
||||
echeance: r.Echeance,
|
||||
reference: r.REFERENCE,
|
||||
typereg: r.TYPEREG,
|
||||
suividatecreation: r.SuiviDateCreation,
|
||||
}));
|
||||
|
||||
let inserted = 0;
|
||||
for (let i = 0; i < allRows.length; i += CHUNK) {
|
||||
await insertChunk(pg, 'mvtreg', allRows.slice(i, i + CHUNK), MVTREG_COLS);
|
||||
inserted += Math.min(CHUNK, allRows.length - i);
|
||||
}
|
||||
|
||||
await logSync(pg, 'mvtreg', inserted, 'ok');
|
||||
console.log(`[mvtreg] ${inserted} nouvelles lignes`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'mvtreg', 0, 'error', err.message);
|
||||
console.error(`[mvtreg] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncMouvements };
|
||||
@@ -0,0 +1,88 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { fullRefresh, batchUpsert, logSync } = require('../utils');
|
||||
|
||||
const NOMEN_COLS = ['no_id','code','libelle','niveau','chemin_pere'];
|
||||
const GAMME_COLS = ['no_id','code','libelle'];
|
||||
const SAISON_COLS = ['no_id','code','libelle'];
|
||||
const AGS_COLS = ['artnoid','idgamme','idsaison'];
|
||||
|
||||
async function syncReferentiel(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === NOMENCLATURE (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT NO_ID, CODE, LIBELLE, NIVEAU, CHEMIN_PERE FROM NOMENCLATURE`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
code: r.CODE,
|
||||
libelle: r.LIBELLE,
|
||||
niveau: r.NIVEAU,
|
||||
chemin_pere: r.CHEMIN_PERE,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'nomenclature', rows, ['no_id'], NOMEN_COLS);
|
||||
await logSync(pg, 'nomenclature', count, 'ok');
|
||||
console.log(`[nomenclature] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'nomenclature', 0, 'error', err.message);
|
||||
console.error(`[nomenclature] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === GAMMES (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT NO_ID, CODE, LIBELLE FROM GAMMES`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
code: r.CODE,
|
||||
libelle: r.LIBELLE,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'gammes', rows, ['no_id'], GAMME_COLS);
|
||||
await logSync(pg, 'gammes', count, 'ok');
|
||||
console.log(`[gammes] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'gammes', 0, 'error', err.message);
|
||||
console.error(`[gammes] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === SAISONS (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT NO_ID, CODE, LIBELLE FROM SAISONS`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
no_id: r.NO_ID,
|
||||
code: r.CODE,
|
||||
libelle: r.LIBELLE,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'saisons', rows, ['no_id'], SAISON_COLS);
|
||||
await logSync(pg, 'saisons', count, 'ok');
|
||||
console.log(`[saisons] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'saisons', 0, 'error', err.message);
|
||||
console.error(`[saisons] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === ART_GAMME_SAISON (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(
|
||||
`SELECT ARTNOID, IDGAMME, IDSAISON FROM ART_GAMME_SAISON`
|
||||
);
|
||||
const rows = res.recordset.map(r => ({
|
||||
artnoid: r.ARTNOID,
|
||||
idgamme: r.IDGAMME,
|
||||
idsaison: r.IDSAISON,
|
||||
}));
|
||||
const count = await fullRefresh(pg, 'art_gamme_saison', rows, AGS_COLS);
|
||||
await logSync(pg, 'art_gamme_saison', count, 'ok');
|
||||
console.log(`[art_gamme_saison] ${count} lignes refresh`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'art_gamme_saison', 0, 'error', err.message);
|
||||
console.error(`[art_gamme_saison] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncReferentiel };
|
||||
@@ -0,0 +1,84 @@
|
||||
const { getMssql, getPg } = require('../db');
|
||||
const { fullRefresh, batchUpsert, logSync } = require('../utils');
|
||||
|
||||
const STOCK_COLS = [
|
||||
'artnoid','site','qte','prmp','valstock','pv',
|
||||
'stockdispo','stockmort','stockcolis',
|
||||
'dernierevente','dernierereception','premierevente',
|
||||
'nbjoursdernierMouvement','nbjoursdernierevente','nbjoursdernierrereception',
|
||||
'interditachat','codefou',
|
||||
];
|
||||
const PA_COLS = ['artnoid','pa'];
|
||||
const PV_COLS = ['artnoid','site','pv'];
|
||||
|
||||
async function syncStock(force) {
|
||||
const ms = await getMssql();
|
||||
const pg = getPg();
|
||||
|
||||
// === Cube_Stock (full refresh — recalculé chaque nuit) ===
|
||||
try {
|
||||
const res = await ms.request().query(`
|
||||
SELECT ArtNoId, Site, QTE, Prmp, ValStock, PV,
|
||||
StockDispo, StockMort, StockColis,
|
||||
DerniereVente, DerniereReception, PremiereVente,
|
||||
NbJoursDernierMouvement, NbJoursDerniereVente,
|
||||
NbJoursDerniereReception, InterditAchat, CODEFOU
|
||||
FROM Cube_Stock
|
||||
`);
|
||||
|
||||
const rows = res.recordset.map(r => ({
|
||||
artnoid: r.ArtNoId,
|
||||
site: r.Site,
|
||||
qte: r.QTE,
|
||||
prmp: r.Prmp,
|
||||
valstock: r.ValStock,
|
||||
pv: r.PV,
|
||||
stockdispo: r.StockDispo,
|
||||
stockmort: r.StockMort,
|
||||
stockcolis: r.StockColis,
|
||||
dernierevente: r.DerniereVente,
|
||||
dernierereception: r.DerniereReception,
|
||||
premierevente: r.PremiereVente,
|
||||
nbjoursdernierMouvement: r.NbJoursDernierMouvement,
|
||||
nbjoursdernierevente: r.NbJoursDerniereVente,
|
||||
nbjoursdernierrereception: r.NbJoursDerniereReception,
|
||||
interditachat: r.InterditAchat,
|
||||
codefou: r.CODEFOU,
|
||||
}));
|
||||
|
||||
const count = await fullRefresh(pg, 'cube_stock', rows, STOCK_COLS);
|
||||
await logSync(pg, 'cube_stock', count, 'ok');
|
||||
console.log(`[cube_stock] ${count} lignes refresh`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cube_stock', 0, 'error', err.message);
|
||||
console.error(`[cube_stock] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === Cube_PA (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(`SELECT ArtNoId, PA FROM Cube_PA`);
|
||||
const rows = res.recordset.map(r => ({ artnoid: r.ArtNoId, pa: r.PA }));
|
||||
const count = await fullRefresh(pg, 'cube_pa', rows, PA_COLS);
|
||||
await logSync(pg, 'cube_pa', count, 'ok');
|
||||
console.log(`[cube_pa] ${count} lignes refresh`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cube_pa', 0, 'error', err.message);
|
||||
console.error(`[cube_pa] ERREUR: ${err.message}`);
|
||||
}
|
||||
|
||||
// === Cube_PV (full refresh) ===
|
||||
try {
|
||||
const res = await ms.request().query(`SELECT ArtNoId, Site, PV FROM Cube_PV`);
|
||||
const rows = res.recordset.map(r => ({
|
||||
artnoid: r.ArtNoId, site: r.Site, pv: r.PV,
|
||||
}));
|
||||
const count = await batchUpsert(pg, 'cube_pv', rows, ['artnoid','site'], PV_COLS);
|
||||
await logSync(pg, 'cube_pv', count, 'ok');
|
||||
console.log(`[cube_pv] ${count} lignes upsert`);
|
||||
} catch (err) {
|
||||
await logSync(pg, 'cube_pv', 0, 'error', err.message);
|
||||
console.error(`[cube_pv] ERREUR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncStock };
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Upsert en masse dans PostgreSQL (chunks de 500 lignes).
|
||||
* @param {import('pg').Pool} pg
|
||||
* @param {string} table - nom de la table PostgreSQL (lowercase)
|
||||
* @param {object[]} rows - lignes avec les valeurs à insérer
|
||||
* @param {string[]} pk - colonnes formant la clé primaire
|
||||
* @param {string[]} cols - toutes les colonnes à upsert
|
||||
* @returns {number} total de lignes traitées
|
||||
*/
|
||||
async function batchUpsert(pg, table, rows, pk, cols) {
|
||||
if (!rows.length) return 0;
|
||||
const CHUNK = 500;
|
||||
let total = 0;
|
||||
for (let i = 0; i < rows.length; i += CHUNK) {
|
||||
const chunk = rows.slice(i, i + CHUNK);
|
||||
const values = [];
|
||||
const placeholders = chunk.map((row, ri) => {
|
||||
const ph = cols.map((col, ci) => {
|
||||
values.push(row[col] ?? null);
|
||||
return `$${ri * cols.length + ci + 1}`;
|
||||
});
|
||||
return `(${ph.join(', ')})`;
|
||||
});
|
||||
const updateCols = cols.filter(c => !pk.includes(c));
|
||||
const updateSet = updateCols.map(c => `${c} = EXCLUDED.${c}`).join(', ');
|
||||
const sql = `
|
||||
INSERT INTO ${table} (${cols.join(', ')})
|
||||
VALUES ${placeholders.join(', ')}
|
||||
ON CONFLICT (${pk.join(', ')}) DO UPDATE SET ${updateSet}
|
||||
`;
|
||||
await pg.query(sql, values);
|
||||
total += chunk.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full refresh : TRUNCATE puis INSERT en masse.
|
||||
*/
|
||||
async function fullRefresh(pg, table, rows, cols) {
|
||||
if (!rows.length) {
|
||||
await pg.query(`TRUNCATE TABLE ${table}`);
|
||||
return 0;
|
||||
}
|
||||
const CHUNK = 500;
|
||||
await pg.query(`TRUNCATE TABLE ${table}`);
|
||||
let total = 0;
|
||||
for (let i = 0; i < rows.length; i += CHUNK) {
|
||||
const chunk = rows.slice(i, i + CHUNK);
|
||||
const values = [];
|
||||
const placeholders = chunk.map((row, ri) => {
|
||||
const ph = cols.map((col, ci) => {
|
||||
values.push(row[col] ?? null);
|
||||
return `$${ri * cols.length + ci + 1}`;
|
||||
});
|
||||
return `(${ph.join(', ')})`;
|
||||
});
|
||||
const sql = `INSERT INTO ${table} (${cols.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
||||
await pg.query(sql, values);
|
||||
total += chunk.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit la date du dernier sync réussi pour une table.
|
||||
*/
|
||||
async function getLastSync(pg, tableName) {
|
||||
const res = await pg.query(
|
||||
`SELECT last_sync FROM sync_log WHERE table_name = $1 AND status = 'ok'`,
|
||||
[tableName]
|
||||
);
|
||||
return res.rows[0]?.last_sync || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre le résultat d'un sync dans sync_log.
|
||||
*/
|
||||
async function logSync(pg, tableName, rowsSynced, status, errorMsg = null) {
|
||||
await pg.query(`
|
||||
INSERT INTO sync_log (table_name, last_sync, rows_synced, status, error_msg)
|
||||
VALUES ($1, NOW(), $2, $3, $4)
|
||||
ON CONFLICT (table_name) DO UPDATE SET
|
||||
last_sync = NOW(),
|
||||
rows_synced = $2,
|
||||
status = $3,
|
||||
error_msg = $4
|
||||
`, [tableName, rowsSynced, status, errorMsg]);
|
||||
}
|
||||
|
||||
module.exports = { batchUpsert, fullRefresh, getLastSync, logSync };
|
||||
Reference in new issue
Block a user