From 9e993b3559e06d8f8e03c9a97ca30cb5b3506f12 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 7 Apr 2026 10:34:36 +0200 Subject: [PATCH] feat: implement advertising data synchronization and API endpoints for performance tracking --- API_DOCUMENTATION.md | 53 ++++++++- postgres/init.sql | 77 ++++++++++++++ src/app.js | 6 +- src/routes/publicites.js | 154 +++++++++++++++++++++++++++ sync/index.js | 2 + sync/tables/publicites.js | 219 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 src/routes/publicites.js create mode 100644 sync/tables/publicites.js diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index c1eb80f..300b9c6 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -3,7 +3,7 @@ **Base URL** : `https://api.ffnancy.fr` **Format** : JSON **Méthode** : GET uniquement (API lecture seule) -**Données** : Synchronisées depuis SQL Server chaque nuit à 3h00 +**Données** : Synchronisées depuis SQL Server chaque nuit à 6h45 (heure Paris) --- @@ -253,7 +253,9 @@ GET /api/articles/:id/mouvements } ``` -**Valeurs `GenreMvt`** : `1` = entrée stock, `3` = vente +**Valeurs `GenreMvt`** : `1` = entrée stock, `3` = vente, `4` = avoir/annulation, `9` = autre vente + +> Le CA TTC (`ca_ttc`) représente les **ventes brutes** (mouvements négatifs des genremvt 3, 4, 9). Le champ `retours_ttc` contient les retours séparément. CA net = `ca_ttc - retours_ttc`. --- @@ -763,6 +765,7 @@ GET /api/performance/ca "qte_vendue": 9640, "ca_ht": 85200.00, "ca_ttc": 97980.00, + "retours_ttc": 1250.00, "marge": 21300.00, "taux_marge": 21.74 } @@ -808,6 +811,39 @@ GET /api/performance/hitparade --- +### CA par fournisseur +``` +GET /api/performance/ca/fournisseur?dateDebut=&dateFin=&site= +``` + +| Paramètre | Type | Description | Défaut | +|-------------|--------|--------------------------|------------| +| `dateDebut` | date | Date début `YYYY-MM-DD` | 2024-01-01 | +| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 | +| `site` | string | Filtrer par site | tous | + +**Réponse** : +```json +{ + "ca_par_fournisseur": [ + { + "fournisseur": "DURACELL", + "code_fournisseur": "D034", + "nb_articles": 12, + "qte_vendue": 840, + "ca_ht": 4200.00, + "ca_ttc": 5040.00, + "marge": 1680.00, + "taux_marge": 40.00 + } + ] +} +``` + +> La jointure se fait via `artfou1.preference = true` (fournisseur principal de l'article) → `fouident.code` pour le nom. + +--- + ### CA par nomenclature (rayon) ``` GET /api/performance/ca/nomenclature?dateDebut=&dateFin=&site=&niveau=1 @@ -996,15 +1032,24 @@ GET /api/sync/status | `cube_stock` | Refresh complet | Nuit | | `cube_pa` | Refresh complet | Nuit | | `cube_pv` | Refresh complet | Nuit | -| `mvtart` | Append (DatMvt >= hier) | Nuit | +| `mvtart` | Append par NO_ID (PK SQL Server) | Nuit | +| `mvtreg` | Append par date | Nuit | | `nomenclature` | Refresh complet | Nuit | | `gammes` | Refresh complet | Nuit | | `art_gamme_saison` | Refresh complet | Nuit | | `saisons` | Refresh complet | Nuit | +| `fouident` | Refresh complet | Nuit | | `cdefou_vivant` | Refresh complet | Nuit | | `cdefou_reception` | Delta | Nuit | | `cdefou_receplig` | Delta | Nuit | +| `commande_fou` | Upsert (NO_ID) | Nuit | +| `cdefou_ligne` | Upsert (NO_ID) | Nuit | +| `commande_auto_qtepropo` | Refresh complet | Nuit | +| `plan_reappro` | Refresh complet | Nuit | | `ranking` | Refresh complet | Nuit | +| `stat_dispoperm` | Upsert | Nuit | +| `phenix_quantite_conseille` | Refresh complet | Nuit | +| `statopca` | Upsert (site+date) | Nuit | --- @@ -1084,7 +1129,7 @@ async function getCommandesFournisseur(codeFou) { ## Notes importantes - Les données sont en lecture seule — aucun endpoint d'écriture -- Les données sont décalées d'au plus 24h (sync à 3h00) +- Les données sont décalées d'au plus 24h (sync automatique à 6h45 heure Paris) - Les champs dates sont au format ISO 8601 UTC - La pagination commence à `page=1` - Le champ `no_id` est l'identifiant interne SQL Server de l'article diff --git a/postgres/init.sql b/postgres/init.sql index af91115..1e11d86 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -447,6 +447,83 @@ CREATE TABLE IF NOT EXISTS commande_auto_qtepropo ( CREATE INDEX IF NOT EXISTS idx_caqp_art_no_id ON commande_auto_qtepropo (art_no_id); CREATE INDEX IF NOT EXISTS idx_caqp_codesite ON commande_auto_qtepropo (codesite); +-- ============================================================ +-- Publicités (FBENTPUB + ECOULEMENT + ECOULEMENT_DETAIL SQL Server) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS pub_entetes ( + ent_npub TEXT PRIMARY KEY, + ent_datdeb TIMESTAMP, + ent_datfin TIMESTAMP, + ent_titre TEXT +); + +CREATE TABLE IF NOT EXISTS pub_ecoulement ( + site TEXT NOT NULL, + tcr_code TEXT NOT NULL, + tcr_libelle TEXT, + tcr_nature SMALLINT, + tcr_type SMALLINT, + tcrd_datedeb TIMESTAMP, + tcrd_datefin TIMESTAMP, + datecalcul TIMESTAMP, + stock_datedebut NUMERIC(14,3), + ca_total_periode_pub NUMERIC(14,4), + ca_pub_periode_pub NUMERIC(14,4), + pourc_capub_catotal NUMERIC(8,4), + qte_vendue_pub NUMERIC(14,3), + client_total_periode NUMERIC(14,3), + client_pub_periode NUMERIC(14,3), + stock_datefin NUMERIC(14,3), + ca_pub_30_jours NUMERIC(14,4), + stock_30_jours NUMERIC(14,3), + ca_pub_60_jours NUMERIC(14,4), + stock_60_jours NUMERIC(14,3), + ca_pub_90_jours NUMERIC(14,4), + stock_90_jours NUMERIC(14,3), + ca_pub_180_jours NUMERIC(14,4), + stock_180_jours NUMERIC(14,3), + ca_depuis_finpub NUMERIC(14,4), + taux_sortie NUMERIC(8,4), + marge NUMERIC(14,4), + taux_marge NUMERIC(8,4), + PRIMARY KEY (site, tcr_code) +); +CREATE INDEX IF NOT EXISTS idx_pub_ecoulement_tcr_code ON pub_ecoulement (tcr_code); +CREATE INDEX IF NOT EXISTS idx_pub_ecoulement_tcrd_datedeb ON pub_ecoulement (tcrd_datedeb); + +CREATE TABLE IF NOT EXISTS pub_ecoulement_detail ( + site TEXT NOT NULL, + tcr_code TEXT NOT NULL, + artnoid BIGINT NOT NULL, + tcr_libelle TEXT, + tcrd_datedeb TIMESTAMP, + tcrd_datefin TIMESTAMP, + datecalcul TIMESTAMP, + ca_pub_periode_pub NUMERIC(14,4), + qte_vendue_pub NUMERIC(14,3), + stock_datedebut NUMERIC(14,3), + stock_datefin NUMERIC(14,3), + ca_pub_30_jours NUMERIC(14,4), + ca_pub_60_jours NUMERIC(14,4), + ca_pub_90_jours NUMERIC(14,4), + ca_pub_180_jours NUMERIC(14,4), + ca_depuis_finpub NUMERIC(14,4), + taux_sortie NUMERIC(8,4), + marge NUMERIC(14,4), + taux_marge NUMERIC(8,4), + codein TEXT, + libelle TEXT, + prmp NUMERIC(12,4), + pa NUMERIC(12,4), + pv NUMERIC(12,4), + prixpub TEXT, + PRIMARY KEY (site, tcr_code, artnoid) +); +CREATE INDEX IF NOT EXISTS idx_pub_detail_tcr_code ON pub_ecoulement_detail (tcr_code); +CREATE INDEX IF NOT EXISTS idx_pub_detail_artnoid ON pub_ecoulement_detail (artnoid); +CREATE INDEX IF NOT EXISTS idx_pub_detail_codein ON pub_ecoulement_detail (codein); + CREATE TABLE IF NOT EXISTS plan_reappro ( id INTEGER PRIMARY KEY, magasin TEXT, diff --git a/src/app.js b/src/app.js index c9d77d0..4ec1250 100644 --- a/src/app.js +++ b/src/app.js @@ -10,7 +10,8 @@ const stockRouter = require('./routes/stock'); const mouvementsRouter = require('./routes/mouvements'); const performanceRouter = require('./routes/performance'); const syncRouter = require('./routes/sync'); -const rankingRouter = require('./routes/ranking'); +const rankingRouter = require('./routes/ranking'); +const publicitesRouter = require('./routes/publicites'); const app = express(); app.use(express.json()); @@ -36,6 +37,7 @@ app.use('/api/mouvements', mouvementsRouter); app.use('/api/performance', performanceRouter); app.use('/api/sync', syncRouter); app.use('/api/ranking', rankingRouter); +app.use('/api/publicites', publicitesRouter); // 404 app.use((req, res) => { @@ -72,5 +74,7 @@ app.listen(PORT, () => { console.log(' GET /api/performance/hitparade[?dateDebut=&dateFin=&site=&limit=&groupBy=qte|ca|marge]'); console.log(' GET /api/performance/ca/nomenclature[?dateDebut=&dateFin=&site=&niveau=]'); console.log(' GET /api/performance/ca/gamme[?dateDebut=&dateFin=&site=]'); + console.log(' GET /api/publicites[?search=&site=&dateDebut=&dateFin=&page=&limit=]'); + console.log(' GET /api/publicites/:code[?site=]'); console.log(' GET /api/sync/status'); }); diff --git a/src/routes/publicites.js b/src/routes/publicites.js new file mode 100644 index 0000000..6316c7f --- /dev/null +++ b/src/routes/publicites.js @@ -0,0 +1,154 @@ +const express = require('express'); +const router = express.Router(); +const { getPool } = require('../config/database'); + +// GET /api/publicites?search=&site=&dateDebut=&dateFin=&page=&limit= +router.get('/', async (req, res) => { + try { + const pool = getPool(); + const { + search = '', + site = '', + dateDebut = '2000-01-01', + dateFin = '2099-12-31', + page = 1, + limit = 50, + } = req.query; + + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500)); + const offset = (pageNum - 1) * limitNum; + + // Synthèse par pub (agrégat multi-sites) + const result = await pool.query(` + SELECT + e.tcr_code, + e.tcr_libelle AS intitule, + e.tcrd_datedeb AS date_debut, + e.tcrd_datefin AS date_fin, + e.datecalcul, + e.site, + e.ca_pub_periode_pub, + e.qte_vendue_pub, + e.ca_total_periode_pub, + ROUND(e.pourc_capub_catotal::NUMERIC, 2) AS pourc_capub_catotal, + e.client_pub_periode, + e.client_total_periode, + e.stock_datedebut, + e.stock_datefin, + e.ca_pub_30_jours, + e.ca_pub_60_jours, + e.ca_pub_90_jours, + e.ca_pub_180_jours, + e.ca_depuis_finpub, + ROUND(e.taux_sortie::NUMERIC, 2) AS taux_sortie, + ROUND(e.marge::NUMERIC, 4) AS marge, + ROUND(e.taux_marge::NUMERIC, 2) AS taux_marge, + (SELECT COUNT(DISTINCT d.artnoid) + FROM pub_ecoulement_detail d + WHERE d.tcr_code = e.tcr_code AND d.site = e.site) AS nb_articles + FROM pub_ecoulement e + WHERE ($1 = '' OR e.tcr_libelle ILIKE '%' || $1 || '%' OR e.tcr_code ILIKE '%' || $1 || '%') + AND ($2 = '' OR e.site ILIKE '%' || $2 || '%') + AND e.tcrd_datedeb >= $3 + AND e.tcrd_datedeb <= $4 + ORDER BY e.tcrd_datedeb DESC, e.tcr_code, e.site + LIMIT $5 OFFSET $6 + `, [search, site, dateDebut, dateFin, limitNum, offset]); + + res.json({ + page: pageNum, + limit: limitNum, + publicites: result.rows, + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/publicites/:code — synthèse par site + articles +router.get('/:code', async (req, res) => { + try { + const pool = getPool(); + const { code } = req.params; + const { site = '' } = req.query; + + // Synthèse par site + const synthese = await pool.query(` + SELECT + e.tcr_code, + e.tcr_libelle AS intitule, + e.tcrd_datedeb AS date_debut, + e.tcrd_datefin AS date_fin, + e.datecalcul, + e.site, + e.ca_pub_periode_pub, + e.qte_vendue_pub, + e.ca_total_periode_pub, + ROUND(e.pourc_capub_catotal::NUMERIC, 2) AS pourc_capub_catotal, + e.client_pub_periode, + e.client_total_periode, + e.stock_datedebut, + e.stock_datefin, + e.ca_pub_30_jours, + e.ca_pub_60_jours, + e.ca_pub_90_jours, + e.ca_pub_180_jours, + e.ca_depuis_finpub, + ROUND(e.taux_sortie::NUMERIC, 2) AS taux_sortie, + ROUND(e.marge::NUMERIC, 4) AS marge, + ROUND(e.taux_marge::NUMERIC, 2) AS taux_marge + FROM pub_ecoulement e + WHERE e.tcr_code = $1 + AND ($2 = '' OR e.site ILIKE '%' || $2 || '%') + ORDER BY e.site + `, [code, site]); + + if (!synthese.rows.length) { + return res.status(404).json({ error: 'Publication introuvable' }); + } + + // Articles de la pub + const articles = await pool.query(` + SELECT + d.site, + d.artnoid, + d.codein, + d.libelle, + d.prmp, + d.pa, + d.pv, + d.prixpub, + d.qte_vendue_pub, + ROUND(d.ca_pub_periode_pub::NUMERIC, 4) AS ca_pub_periode_pub, + d.stock_datedebut, + d.stock_datefin, + d.ca_pub_30_jours, + d.ca_pub_60_jours, + d.ca_pub_90_jours, + d.ca_pub_180_jours, + d.ca_depuis_finpub, + ROUND(d.taux_sortie::NUMERIC, 2) AS taux_sortie, + ROUND(d.marge::NUMERIC, 4) AS marge, + ROUND(d.taux_marge::NUMERIC, 2) AS taux_marge + FROM pub_ecoulement_detail d + WHERE d.tcr_code = $1 + AND ($2 = '' OR d.site ILIKE '%' || $2 || '%') + ORDER BY d.site, d.ca_pub_periode_pub DESC + `, [code, site]); + + res.json({ + tcr_code: synthese.rows[0].tcr_code, + intitule: synthese.rows[0].intitule, + date_debut: synthese.rows[0].date_debut, + date_fin: synthese.rows[0].date_fin, + datecalcul: synthese.rows[0].datecalcul, + synthese_par_site: synthese.rows, + articles: articles.rows, + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +module.exports = router; diff --git a/sync/index.js b/sync/index.js index 805c27c..b28cc67 100644 --- a/sync/index.js +++ b/sync/index.js @@ -11,6 +11,7 @@ const { syncRanking } = require('./tables/ranking'); const { syncStatDispoperm } = require('./tables/stat_dispoperm'); const { syncPhenixQuantiteConseille } = require('./tables/phenix_quantite_conseille'); const { syncAppro } = require('./tables/appro'); +const { syncPublicites } = require('./tables/publicites'); const { closeAll } = require('./db'); const { startDashboard, registerSyncFn, @@ -33,6 +34,7 @@ const STEPS = [ { name: 'stat_dispoperm', fn: syncStatDispoperm }, { name: 'phenix_quantite_conseille', fn: syncPhenixQuantiteConseille }, { name: 'appro', fn: syncAppro }, + { name: 'publicites', fn: syncPublicites }, ]; async function syncAll(force = false) { diff --git a/sync/tables/publicites.js b/sync/tables/publicites.js new file mode 100644 index 0000000..8b76409 --- /dev/null +++ b/sync/tables/publicites.js @@ -0,0 +1,219 @@ +const { getMssql, getPg } = require('../db'); +const { logSync } = require('../utils'); + +// ============================================================ +// pub_entetes — FBENTPUB (refresh complet) +// ============================================================ +const ENTETES_COLS = ['ent_npub', 'ent_datdeb', 'ent_datfin', 'ent_titre']; + +// ============================================================ +// pub_ecoulement — ECOULEMENT (refresh complet) +// ============================================================ +const ECOULEMENT_COLS = [ + 'site', 'tcr_code', 'tcr_libelle', 'tcr_nature', 'tcr_type', + 'tcrd_datedeb', 'tcrd_datefin', 'datecalcul', + 'stock_datedebut', 'ca_total_periode_pub', 'ca_pub_periode_pub', + 'pourc_capub_catotal', 'qte_vendue_pub', 'client_total_periode', + 'client_pub_periode', 'stock_datefin', + 'ca_pub_30_jours', 'stock_30_jours', + 'ca_pub_60_jours', 'stock_60_jours', + 'ca_pub_90_jours', 'stock_90_jours', + 'ca_pub_180_jours', 'stock_180_jours', + 'ca_depuis_finpub', 'taux_sortie', 'marge', 'taux_marge', +]; + +// ============================================================ +// pub_ecoulement_detail — ECOULEMENT_DETAIL (refresh complet) +// ============================================================ +const DETAIL_COLS = [ + 'site', 'tcr_code', 'artnoid', 'tcr_libelle', + 'tcrd_datedeb', 'tcrd_datefin', 'datecalcul', + 'ca_pub_periode_pub', 'qte_vendue_pub', + 'stock_datedebut', 'stock_datefin', + 'ca_pub_30_jours', 'ca_pub_60_jours', 'ca_pub_90_jours', 'ca_pub_180_jours', + 'ca_depuis_finpub', 'taux_sortie', 'marge', 'taux_marge', + 'codein', 'libelle', 'prmp', 'pa', 'pv', 'prixpub', +]; + +const CHUNK = 500; + +async function insertChunk(pg, table, rows, cols, conflictTarget) { + 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(', ')})`; + }); + const colList = cols.join(', '); + const updateSet = cols + .filter(c => !conflictTarget.split(', ').includes(c)) + .map(c => `${c}=EXCLUDED.${c}`) + .join(', '); + await pg.query( + `INSERT INTO ${table} (${colList}) VALUES ${placeholders.join(', ')} + ON CONFLICT (${conflictTarget}) DO UPDATE SET ${updateSet}`, + values + ); +} + +async function syncPublicites(force) { + const ms = await getMssql(); + const pg = getPg(); + + // --- pub_entetes (FBENTPUB) --- + try { + const res = await ms.request().query(` + SELECT ENT_NPUB, ENT_DATDEB, ENT_DATFIN, ENT_TITRE + FROM FBENTPUB + `); + + const rows = res.recordset.map(r => ({ + ent_npub: String(r.ENT_NPUB), + ent_datdeb: r.ENT_DATDEB && r.ENT_DATDEB.getFullYear() > 1901 ? r.ENT_DATDEB : null, + ent_datfin: r.ENT_DATFIN && r.ENT_DATFIN.getFullYear() > 1901 ? r.ENT_DATFIN : null, + ent_titre: r.ENT_TITRE, + })); + + await pg.query('TRUNCATE TABLE pub_entetes'); + 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 = ENTETES_COLS.map((col, ci) => { + values.push(row[col] ?? null); + return `$${ri * ENTETES_COLS.length + ci + 1}`; + }); + return `(${ph.join(', ')})`; + }); + await pg.query( + `INSERT INTO pub_entetes (${ENTETES_COLS.join(', ')}) VALUES ${placeholders.join(', ')}`, + values + ); + } + + await logSync(pg, 'pub_entetes', rows.length, 'ok'); + console.log(`[pub_entetes] ${rows.length} lignes`); + } catch (err) { + await logSync(pg, 'pub_entetes', 0, 'error', err.message); + console.error(`[pub_entetes] ERREUR: ${err.message}`); + } + + // --- pub_ecoulement (ECOULEMENT) --- + try { + const res = await ms.request().query(` + SELECT + SITE, TCR_CODE, TCR_LIBELLE, TCR_NATURE, TCR_TYPE, + TCRD_DATEDEB, TCRD_DATEFIN, DATECALCUL, + STOCK_DATEDEBUT, CA_TOTAL_PERIODE_PUB, CA_PUB_PERIODE_PUB, + POURC_CAPUB_CATOTAL, QTE_VENDUE_PUB, CLIENT_TOTAL_PERIODE, + CLIENT_PUB_PERIODE, STOCK_DATEFIN, + CA_PUB_30_JOURS, STOCK_30_JOURS, + CA_PUB_60_JOURS, STOCK_60_JOURS, + CA_PUB_90_JOURS, STOCK_90_JOURS, + CA_PUB_180_JOURS, STOCK_180_JOURS, + CA_DEPUIS_FINPUB, TAUX_SORTIE, MARGE, TAUX_MARGE + FROM ECOULEMENT + `); + + const rows = res.recordset.map(r => ({ + site: r.SITE, + tcr_code: r.TCR_CODE, + tcr_libelle: r.TCR_LIBELLE, + tcr_nature: r.TCR_NATURE, + tcr_type: r.TCR_TYPE, + tcrd_datedeb: r.TCRD_DATEDEB, + tcrd_datefin: r.TCRD_DATEFIN, + datecalcul: r.DATECALCUL, + stock_datedebut: r.STOCK_DATEDEBUT, + ca_total_periode_pub: r.CA_TOTAL_PERIODE_PUB, + ca_pub_periode_pub: r.CA_PUB_PERIODE_PUB, + pourc_capub_catotal: r.POURC_CAPUB_CATOTAL, + qte_vendue_pub: r.QTE_VENDUE_PUB, + client_total_periode: r.CLIENT_TOTAL_PERIODE, + client_pub_periode: r.CLIENT_PUB_PERIODE, + stock_datefin: r.STOCK_DATEFIN, + ca_pub_30_jours: r.CA_PUB_30_JOURS, + stock_30_jours: r.STOCK_30_JOURS, + ca_pub_60_jours: r.CA_PUB_60_JOURS, + stock_60_jours: r.STOCK_60_JOURS, + ca_pub_90_jours: r.CA_PUB_90_JOURS, + stock_90_jours: r.STOCK_90_JOURS, + ca_pub_180_jours: r.CA_PUB_180_JOURS, + stock_180_jours: r.STOCK_180_JOURS, + ca_depuis_finpub: r.CA_DEPUIS_FINPUB, + taux_sortie: r.TAUX_SORTIE, + marge: r.MARGE, + taux_marge: r.TAUX_MARGE, + })); + + await pg.query('TRUNCATE TABLE pub_ecoulement_detail'); + await pg.query('TRUNCATE TABLE pub_ecoulement'); + for (let i = 0; i < rows.length; i += CHUNK) { + await insertChunk(pg, 'pub_ecoulement', rows.slice(i, i + CHUNK), ECOULEMENT_COLS, 'site, tcr_code'); + } + + await logSync(pg, 'pub_ecoulement', rows.length, 'ok'); + console.log(`[pub_ecoulement] ${rows.length} lignes`); + } catch (err) { + await logSync(pg, 'pub_ecoulement', 0, 'error', err.message); + console.error(`[pub_ecoulement] ERREUR: ${err.message}`); + } + + // --- pub_ecoulement_detail (ECOULEMENT_DETAIL) --- + try { + const res = await ms.request().query(` + SELECT + SITE, TCR_CODE, ARTNOID, TCR_LIBELLE, + TCRD_DATEDEB, TCRD_DATEFIN, DATECALCUL, + CA_PUB_PERIODE_PUB, QTE_VENDUE_PUB, + STOCK_DATEDEBUT, STOCK_DATEFIN, + CA_PUB_30_JOURS, CA_PUB_60_JOURS, CA_PUB_90_JOURS, CA_PUB_180_JOURS, + CA_DEPUIS_FINPUB, TAUX_SORTIE, MARGE, TAUX_MARGE, + CODEIN, LIBELLE, PRMP, PA, PV, PRIXPUB + FROM ECOULEMENT_DETAIL + `); + + const rows = res.recordset.map(r => ({ + site: r.SITE, + tcr_code: r.TCR_CODE, + artnoid: r.ARTNOID, + tcr_libelle: r.TCR_LIBELLE, + tcrd_datedeb: r.TCRD_DATEDEB, + tcrd_datefin: r.TCRD_DATEFIN, + datecalcul: r.DATECALCUL, + ca_pub_periode_pub: r.CA_PUB_PERIODE_PUB, + qte_vendue_pub: r.QTE_VENDUE_PUB, + stock_datedebut: r.STOCK_DATEDEBUT, + stock_datefin: r.STOCK_DATEFIN, + ca_pub_30_jours: r.CA_PUB_30_JOURS, + ca_pub_60_jours: r.CA_PUB_60_JOURS, + ca_pub_90_jours: r.CA_PUB_90_JOURS, + ca_pub_180_jours: r.CA_PUB_180_JOURS, + ca_depuis_finpub: r.CA_DEPUIS_FINPUB, + taux_sortie: r.TAUX_SORTIE, + marge: r.MARGE, + taux_marge: r.TAUX_MARGE, + codein: r.CODEIN, + libelle: r.LIBELLE, + prmp: r.PRMP, + pa: r.PA, + pv: r.PV, + prixpub: r.PRIXPUB, + })); + + for (let i = 0; i < rows.length; i += CHUNK) { + await insertChunk(pg, 'pub_ecoulement_detail', rows.slice(i, i + CHUNK), DETAIL_COLS, 'site, tcr_code, artnoid'); + } + + await logSync(pg, 'pub_ecoulement_detail', rows.length, 'ok'); + console.log(`[pub_ecoulement_detail] ${rows.length} lignes`); + } catch (err) { + await logSync(pg, 'pub_ecoulement_detail', 0, 'error', err.message); + console.error(`[pub_ecoulement_detail] ERREUR: ${err.message}`); + } +} + +module.exports = { syncPublicites };