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,16 @@
|
|||||||
|
# Copier en .env.docker et remplir les valeurs
|
||||||
|
# .env.docker est exclu du git (contient les mots de passe)
|
||||||
|
|
||||||
|
DB_HOST=192.168.184.254
|
||||||
|
DB_PORT=1433
|
||||||
|
DB_NAME=foirfouille
|
||||||
|
DB_USER=sa
|
||||||
|
DB_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
PG_HOST=postgres
|
||||||
|
PG_PORT=5432
|
||||||
|
PG_DB=foirfouille
|
||||||
|
PG_USER=ff_api
|
||||||
|
PG_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||||
|
|
||||||
|
API_PORT=3000
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
sync/node_modules/
|
||||||
|
|
||||||
|
# Fichiers secrets — ne jamais committer
|
||||||
|
.env
|
||||||
|
.env.docker
|
||||||
|
frpc.toml
|
||||||
|
|
||||||
|
# Scripts et docs d'exploration (phase de développement, pas nécessaires en prod)
|
||||||
|
scripts/
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Fichiers de déploiement locaux
|
||||||
|
*.log
|
||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
COPY src/ ./src/
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "src/app.js"]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ff_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${PG_DB}
|
||||||
|
POSTGRES_USER: ${PG_USER}
|
||||||
|
POSTGRES_PASSWORD: ${PG_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
container_name: ff_api
|
||||||
|
environment:
|
||||||
|
PG_HOST: postgres
|
||||||
|
PG_PORT: 5432
|
||||||
|
PG_DB: ${PG_DB}
|
||||||
|
PG_USER: ${PG_USER}
|
||||||
|
PG_PASSWORD: ${PG_PASSWORD}
|
||||||
|
API_PORT: ${API_PORT:-3000}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
sync:
|
||||||
|
build: ./sync
|
||||||
|
container_name: ff_sync
|
||||||
|
environment:
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_PORT: ${DB_PORT:-1433}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
PG_HOST: postgres
|
||||||
|
PG_PORT: 5432
|
||||||
|
PG_DB: ${PG_DB}
|
||||||
|
PG_USER: ${PG_USER}
|
||||||
|
PG_PASSWORD: ${PG_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Tunnel sortant vers le serveur Hostinger (aucun port entrant requis)
|
||||||
|
frpc:
|
||||||
|
image: snowdreamtech/frpc:latest
|
||||||
|
container_name: ff_frpc
|
||||||
|
volumes:
|
||||||
|
- ./frpc.toml:/etc/frp/frpc.toml:ro
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Copier en frpc.toml et remplir les valeurs
|
||||||
|
# frpc.toml est exclu du git (contient le token secret)
|
||||||
|
|
||||||
|
serverAddr = "api.ffnancy.fr"
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
auth.method = "token"
|
||||||
|
auth.token = "CHANGE_ME_FRP_SECRET_TOKEN"
|
||||||
|
|
||||||
|
log.level = "info"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "foirfouille-api"
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "api"
|
||||||
|
localPort = 3000
|
||||||
|
remotePort = 13000
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "foirfouille-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/app.js",
|
||||||
|
"dev": "node --watch src/app.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"pg": "^8.13.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Schéma PostgreSQL — foirfouille API
|
||||||
|
-- Tables sélectionnées depuis SQL Server (copie nightly via sync)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Contrôle de synchronisation
|
||||||
|
CREATE TABLE IF NOT EXISTS sync_log (
|
||||||
|
table_name TEXT PRIMARY KEY,
|
||||||
|
last_sync TIMESTAMP,
|
||||||
|
rows_synced INTEGER,
|
||||||
|
status TEXT,
|
||||||
|
error_msg TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Référentiel articles
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS articles (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
codein TEXT,
|
||||||
|
libelle1 TEXT,
|
||||||
|
libelle2 TEXT,
|
||||||
|
lib_ticket TEXT,
|
||||||
|
tax_code TEXT,
|
||||||
|
ach_code TEXT,
|
||||||
|
utilisable TEXT,
|
||||||
|
actif TEXT,
|
||||||
|
suspendu TEXT,
|
||||||
|
suividatecreation TIMESTAMP,
|
||||||
|
suividatemodif TIMESTAMP,
|
||||||
|
nom_no_id BIGINT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_codein ON articles (codein);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_libelle1 ON articles (libelle1);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_nom_no_id ON articles (nom_no_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS article_infosup (
|
||||||
|
artnoid BIGINT PRIMARY KEY,
|
||||||
|
prix_vente_mini NUMERIC(12,4),
|
||||||
|
prix_vente_maxi NUMERIC(12,4),
|
||||||
|
eco_ht NUMERIC(12,4),
|
||||||
|
eco_ttc NUMERIC(12,4),
|
||||||
|
on_web TEXT,
|
||||||
|
interdit_remise TEXT,
|
||||||
|
nomphoto TEXT,
|
||||||
|
datedebvente TIMESTAMP,
|
||||||
|
datefinvente TIMESTAMP,
|
||||||
|
datevaliditeachat TIMESTAMP,
|
||||||
|
commentaire TEXT,
|
||||||
|
pv_conseille NUMERIC(12,4),
|
||||||
|
fraislogistic NUMERIC(12,4),
|
||||||
|
transit NUMERIC(12,4),
|
||||||
|
distribution NUMERIC(12,4)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS art_gtin (
|
||||||
|
idarticle BIGINT,
|
||||||
|
gtin TEXT,
|
||||||
|
preferentiel SMALLINT,
|
||||||
|
PRIMARY KEY (idarticle, gtin)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_art_gtin_article ON art_gtin (idarticle);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Nomenclature & Gammes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS nomenclature (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
code TEXT,
|
||||||
|
libelle TEXT,
|
||||||
|
niveau INTEGER,
|
||||||
|
chemin_pere TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nomenclature_niveau ON nomenclature (niveau);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS gammes (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
code TEXT,
|
||||||
|
libelle TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS saisons (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
code TEXT,
|
||||||
|
libelle TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS art_gamme_saison (
|
||||||
|
artnoid BIGINT,
|
||||||
|
idgamme BIGINT,
|
||||||
|
idsaison BIGINT,
|
||||||
|
PRIMARY KEY (artnoid, idgamme, idsaison)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ags_artnoid ON art_gamme_saison (artnoid);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Fournisseurs
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS artfou1 (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
art_no_id BIGINT,
|
||||||
|
code TEXT,
|
||||||
|
reference TEXT,
|
||||||
|
ean13 TEXT,
|
||||||
|
itf TEXT,
|
||||||
|
qteua NUMERIC(10,3),
|
||||||
|
pcb NUMERIC(10,3),
|
||||||
|
spcb NUMERIC(10,3),
|
||||||
|
delai INTEGER,
|
||||||
|
securite NUMERIC(10,3),
|
||||||
|
preference INTEGER,
|
||||||
|
suspendu TEXT,
|
||||||
|
suividatecreation TIMESTAMP,
|
||||||
|
suividatemodif TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_artfou1_art_no_id ON artfou1 (art_no_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_artfou1_code ON artfou1 (code);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS artfou2 (
|
||||||
|
idartfou1 BIGINT PRIMARY KEY,
|
||||||
|
prixachat NUMERIC(12,4),
|
||||||
|
remise_promotion NUMERIC(10,4)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fouadr1 (
|
||||||
|
code TEXT,
|
||||||
|
sit_code TEXT,
|
||||||
|
raisonsociale TEXT,
|
||||||
|
adrligne1 TEXT,
|
||||||
|
telephone TEXT,
|
||||||
|
email TEXT,
|
||||||
|
PRIMARY KEY (code, sit_code)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Cubes de stock et prix
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cube_stock (
|
||||||
|
artnoid BIGINT,
|
||||||
|
site TEXT,
|
||||||
|
qte NUMERIC(12,3),
|
||||||
|
prmp NUMERIC(12,4),
|
||||||
|
valstock NUMERIC(14,4),
|
||||||
|
pv NUMERIC(12,4),
|
||||||
|
stockdispo NUMERIC(12,3),
|
||||||
|
stockmort NUMERIC(12,3),
|
||||||
|
stockcolis NUMERIC(12,3),
|
||||||
|
dernierevente TIMESTAMP,
|
||||||
|
dernierereception TIMESTAMP,
|
||||||
|
premierevente TIMESTAMP,
|
||||||
|
nbjoursdernierMouvement INTEGER,
|
||||||
|
nbjoursdernierevente INTEGER,
|
||||||
|
nbjoursdernierrereception INTEGER,
|
||||||
|
interditachat TEXT,
|
||||||
|
codefou TEXT,
|
||||||
|
PRIMARY KEY (artnoid, site)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cube_stock_site ON cube_stock (site);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cube_pa (
|
||||||
|
artnoid BIGINT PRIMARY KEY,
|
||||||
|
pa NUMERIC(12,4)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cube_pv (
|
||||||
|
artnoid BIGINT,
|
||||||
|
site TEXT,
|
||||||
|
pv NUMERIC(12,4),
|
||||||
|
PRIMARY KEY (artnoid, site)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cube_pv_site ON cube_pv (site);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Mouvements
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mvtart (
|
||||||
|
artnoid BIGINT,
|
||||||
|
datmvt TIMESTAMP,
|
||||||
|
site TEXT,
|
||||||
|
libmvt TEXT,
|
||||||
|
genremvt INTEGER,
|
||||||
|
qtemvt NUMERIC(12,3),
|
||||||
|
valmvt NUMERIC(14,4),
|
||||||
|
mntmvtht NUMERIC(14,4),
|
||||||
|
mntmvtttc NUMERIC(14,4),
|
||||||
|
margemvt NUMERIC(14,4),
|
||||||
|
qtestock NUMERIC(12,3),
|
||||||
|
prmp NUMERIC(12,4),
|
||||||
|
valstock NUMERIC(14,4),
|
||||||
|
codefou TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mvtart_artnoid ON mvtart (artnoid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mvtart_datmvt ON mvtart (datmvt);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mvtart_site ON mvtart (site);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mvtart_genremvt ON mvtart (genremvt);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mvtreg (
|
||||||
|
datmvt TIMESTAMP,
|
||||||
|
codtick TEXT,
|
||||||
|
codcartecli TEXT,
|
||||||
|
coddev TEXT,
|
||||||
|
mntreg NUMERIC(14,4),
|
||||||
|
mntregdev NUMERIC(14,4),
|
||||||
|
clientnom TEXT,
|
||||||
|
echeance TIMESTAMP,
|
||||||
|
reference TEXT,
|
||||||
|
typereg TEXT,
|
||||||
|
suividatecreation TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mvtreg_datmvt ON mvtreg (datmvt);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Commandes fournisseurs
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cdefou_vivant (
|
||||||
|
cdefou_ligne_com_no_id BIGINT,
|
||||||
|
artfou1_no_id BIGINT,
|
||||||
|
articles_codein TEXT,
|
||||||
|
articles_libelle1 TEXT,
|
||||||
|
articles_libelle2 TEXT,
|
||||||
|
artfou1_reference TEXT,
|
||||||
|
artfou1_ean13 TEXT,
|
||||||
|
artfou1_itf TEXT,
|
||||||
|
cdefou_ligne_qtecde NUMERIC(12,3),
|
||||||
|
cdefou_ligne_prixbrut NUMERIC(12,4),
|
||||||
|
cdefou_ligne_remise NUMERIC(10,4),
|
||||||
|
cdefou_ligne_remise2 NUMERIC(10,4),
|
||||||
|
cdefou_ligne_remise3 NUMERIC(10,4),
|
||||||
|
cdefou_ligne_prixnet NUMERIC(12,4),
|
||||||
|
cdefou_ligne_montant NUMERIC(14,4),
|
||||||
|
cdefou_ligne_prixvente NUMERIC(12,4),
|
||||||
|
cdefou_ligne_gratuit NUMERIC(12,3),
|
||||||
|
cdefou_ligne_qteacc NUMERIC(12,3),
|
||||||
|
cdefou_ligne_qteann NUMERIC(12,3),
|
||||||
|
cdefou_ligne_qteatt NUMERIC(12,3),
|
||||||
|
cdefou_ligne_qterel NUMERIC(12,3),
|
||||||
|
cdefou_ligne_cdeligtard TIMESTAMP,
|
||||||
|
cdefou_ligne_cdeligtot TIMESTAMP,
|
||||||
|
cotisation_logistique NUMERIC(14,4),
|
||||||
|
fraislogistic NUMERIC(12,4),
|
||||||
|
transit NUMERIC(12,4),
|
||||||
|
distribution NUMERIC(12,4),
|
||||||
|
taxe NUMERIC(12,4),
|
||||||
|
commentaire TEXT,
|
||||||
|
suividatecreation TIMESTAMP,
|
||||||
|
PRIMARY KEY (cdefou_ligne_com_no_id, artfou1_no_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cdefou_vivant_artfou1 ON cdefou_vivant (artfou1_no_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cdefou_vivant_date ON cdefou_vivant (suividatecreation);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cdefou_reception (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
suividatecreation TIMESTAMP,
|
||||||
|
suividatemodif TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cdefou_receplig (
|
||||||
|
no_id BIGINT PRIMARY KEY,
|
||||||
|
cdefou_reception_no_id BIGINT,
|
||||||
|
artfou1_no_id BIGINT,
|
||||||
|
qtebl NUMERIC(12,3),
|
||||||
|
qterec NUMERIC(12,3),
|
||||||
|
qteacc NUMERIC(12,3),
|
||||||
|
qteref NUMERIC(12,3),
|
||||||
|
qteaff NUMERIC(12,3),
|
||||||
|
pribrut NUMERIC(12,4),
|
||||||
|
remise NUMERIC(10,4),
|
||||||
|
prirec NUMERIC(12,4),
|
||||||
|
mntrec NUMERIC(14,4),
|
||||||
|
motifrefus TEXT,
|
||||||
|
recpb SMALLINT,
|
||||||
|
recpbok SMALLINT,
|
||||||
|
suividatecreation TIMESTAMP,
|
||||||
|
suividatemodif TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_receplig_artfou1 ON cdefou_receplig (artfou1_no_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_receplig_date ON cdefou_receplig (suividatecreation);
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const { getPool } = require('./config/database');
|
||||||
|
|
||||||
|
const schemaRouter = require('./routes/schema');
|
||||||
|
const articlesRouter = require('./routes/articles');
|
||||||
|
const fournisseursRouter = require('./routes/fournisseurs');
|
||||||
|
const commandesRouter = require('./routes/commandes');
|
||||||
|
const stockRouter = require('./routes/stock');
|
||||||
|
const mouvementsRouter = require('./routes/mouvements');
|
||||||
|
const performanceRouter = require('./routes/performance');
|
||||||
|
const syncRouter = require('./routes/sync');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/api/health', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
await pool.query('SELECT 1 AS ok');
|
||||||
|
res.json({ status: 'ok', database: process.env.PG_DB, server: process.env.PG_HOST });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ status: 'error', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/schema', schemaRouter);
|
||||||
|
app.use('/api/articles', articlesRouter);
|
||||||
|
app.use('/api/fournisseurs', fournisseursRouter);
|
||||||
|
app.use('/api/commandes', commandesRouter);
|
||||||
|
app.use('/api/stock', stockRouter);
|
||||||
|
app.use('/api/mouvements', mouvementsRouter);
|
||||||
|
app.use('/api/performance', performanceRouter);
|
||||||
|
app.use('/api/sync', syncRouter);
|
||||||
|
|
||||||
|
// 404
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'Route introuvable' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.API_PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`\nAPI Foirfouille démarrée sur http://localhost:${PORT}`);
|
||||||
|
console.log(`Base : ${process.env.PG_HOST}/${process.env.PG_DB} (PostgreSQL)\n`);
|
||||||
|
console.log('Endpoints disponibles :');
|
||||||
|
console.log(' GET /api/health');
|
||||||
|
console.log(' GET /api/schema/tables[?search=]');
|
||||||
|
console.log(' GET /api/schema/top[?limit=]');
|
||||||
|
console.log(' GET /api/schema/tables/:nom');
|
||||||
|
console.log(' GET /api/articles[?search=&codein=&ean=&codefou=&actif=&page=&limit=]');
|
||||||
|
console.log(' GET /api/articles/:id');
|
||||||
|
console.log(' GET /api/articles/:id/referentiel');
|
||||||
|
console.log(' GET /api/articles/:id/mouvements');
|
||||||
|
console.log(' GET /api/fournisseurs[?search=]');
|
||||||
|
console.log(' GET /api/fournisseurs/:code/articles');
|
||||||
|
console.log(' GET /api/fournisseurs/:code/commandes');
|
||||||
|
console.log(' GET /api/commandes[?dateDebut=&dateFin=&codefou=]');
|
||||||
|
console.log(' GET /api/commandes/:noCommande');
|
||||||
|
console.log(' GET /api/commandes/receptions/liste');
|
||||||
|
console.log(' GET /api/stock[?site=]');
|
||||||
|
console.log(' GET /api/stock/article/:id');
|
||||||
|
console.log(' GET /api/stock/site/:site');
|
||||||
|
console.log(' GET /api/stock/valorisation');
|
||||||
|
console.log(' GET /api/mouvements/articles[?dateDebut=&dateFin=&site=]');
|
||||||
|
console.log(' GET /api/mouvements/reglements[?dateDebut=&dateFin=]');
|
||||||
|
console.log(' GET /api/mouvements/synthese[?dateDebut=&dateFin=&site=]');
|
||||||
|
console.log(' GET /api/performance/ca[?dateDebut=&dateFin=&site=&groupBy=jour|mois]');
|
||||||
|
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/sync/status');
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
let pool = null;
|
||||||
|
|
||||||
|
function getPool() {
|
||||||
|
if (!pool) {
|
||||||
|
pool = new Pool({
|
||||||
|
host: process.env.PG_HOST || 'postgres',
|
||||||
|
port: parseInt(process.env.PG_PORT) || 5432,
|
||||||
|
database: process.env.PG_DB || 'foirfouille',
|
||||||
|
user: process.env.PG_USER || 'ff_api',
|
||||||
|
password: process.env.PG_PASSWORD,
|
||||||
|
max: 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
});
|
||||||
|
pool.on('error', (err) => {
|
||||||
|
console.error('PostgreSQL pool error:', err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getPool };
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&page=&limit=
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { search = '', codein = '', ean = '', actif = '', codefou = '', 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 offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
||||||
|
a.TAX_CODE, a.ACH_CODE, a.UTILISABLE, a.ACTIF, a.SUSPENDU,
|
||||||
|
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
||||||
|
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI, ai.ECO_TTC,
|
||||||
|
ai.ON_WEB, ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
||||||
|
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||||
|
pa.PA,
|
||||||
|
g.GTIN,
|
||||||
|
fpref.CODE AS CODEFOU_PRINCIPAL,
|
||||||
|
fpref.NOM_FOU AS NOM_FOU_PRINCIPAL,
|
||||||
|
fpref.REF AS REF_FOU_PRINCIPALE,
|
||||||
|
fpref.PCB AS PCB_PRINCIPAL
|
||||||
|
FROM ARTICLES a
|
||||||
|
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||||
|
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||||
|
LEFT JOIN ART_GTIN g ON g.IDARTICLE = a.NO_ID AND g.PREFERENTIEL = 1
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT f.CODE, f.REFERENCE AS REF, f.QTEUA AS PCB,
|
||||||
|
fa.RAISONSOCIALE AS NOM_FOU
|
||||||
|
FROM ARTFOU1 f
|
||||||
|
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||||
|
WHERE f.ART_NO_ID = a.NO_ID
|
||||||
|
ORDER BY f.PREFERENCE
|
||||||
|
LIMIT 1
|
||||||
|
) fpref ON true
|
||||||
|
WHERE
|
||||||
|
($1 = '%%' OR a.LIBELLE1 LIKE $1 OR a.LIBELLE2 LIKE $1)
|
||||||
|
AND ($2 = '%%' OR a.CODEIN LIKE $2)
|
||||||
|
AND ($3 = '%%' OR g.GTIN LIKE $3)
|
||||||
|
AND ($5 = '' OR (
|
||||||
|
$5 = '1' AND a.ACTIF IS NOT NULL AND a.SUSPENDU IS NULL
|
||||||
|
) OR (
|
||||||
|
$5 = '0' AND a.SUSPENDU IS NOT NULL
|
||||||
|
))
|
||||||
|
AND ($4 = '%%' OR EXISTS (
|
||||||
|
SELECT 1 FROM ARTFOU1 f WHERE f.ART_NO_ID = a.NO_ID AND f.CODE LIKE $4
|
||||||
|
))
|
||||||
|
ORDER BY a.LIBELLE1
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, articles: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/articles/:id - Détail complet d'un article
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const article = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.*, ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI,
|
||||||
|
ai.ECO_HT, ai.ECO_TTC, ai.ON_WEB, ai.INTERDIT_REMISE,
|
||||||
|
ai.NOMPHOTO, ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||||
|
ai.DATEVALIDITEACHAT, ai.COMMENTAIRE, ai.PV_CONSEILLE,
|
||||||
|
ai.FRAISLOGISTIC, ai.TRANSIT, ai.DISTRIBUTION,
|
||||||
|
pa.PA
|
||||||
|
FROM ARTICLES a
|
||||||
|
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||||
|
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||||
|
WHERE a.NO_ID = $1
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
if (!article.rows.length)
|
||||||
|
return res.status(404).json({ error: 'Article introuvable' });
|
||||||
|
|
||||||
|
const gtins = await pool.query(
|
||||||
|
`SELECT GTIN, PREFERENTIEL FROM ART_GTIN WHERE IDARTICLE = $1 ORDER BY PREFERENTIEL DESC`,
|
||||||
|
[req.params.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const stock = await pool.query(`
|
||||||
|
SELECT Site, QTE, Prmp, ValStock, PV, StockDispo,
|
||||||
|
DerniereVente, DerniereReception, StockMort
|
||||||
|
FROM Cube_Stock WHERE ArtNoId = $1 ORDER BY Site
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
const prix = await pool.query(
|
||||||
|
`SELECT Site, PV FROM Cube_PV WHERE ArtNoId = $1 ORDER BY Site`,
|
||||||
|
[req.params.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fournisseurs = await pool.query(`
|
||||||
|
SELECT f.CODE AS CODEFOU, fa.RAISONSOCIALE AS NOM_FOU,
|
||||||
|
f.REFERENCE, f.EAN13, f.ITF,
|
||||||
|
f.QTEUA, f.PCB, f.SPCB, f.DELAI, f.SECURITE, f.PREFERENCE, f.SUSPENDU,
|
||||||
|
t.PRIXACHAT, t.REMISE_PROMOTION
|
||||||
|
FROM ARTFOU1 f
|
||||||
|
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||||
|
LEFT JOIN ARTFOU2 t ON t.IDARTFOU1 = f.NO_ID
|
||||||
|
WHERE f.ART_NO_ID = $1
|
||||||
|
ORDER BY f.PREFERENCE
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
article: article.rows[0],
|
||||||
|
gtins: gtins.rows,
|
||||||
|
stock: stock.rows,
|
||||||
|
prix: prix.rows,
|
||||||
|
fournisseurs: fournisseurs.rows,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/articles/:id/referentiel - Fiche complète
|
||||||
|
router.get('/:id/referentiel', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const article = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
||||||
|
a.TAX_CODE, a.ACTIF, a.SUSPENDU, a.UTILISABLE,
|
||||||
|
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
||||||
|
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI,
|
||||||
|
ai.ECO_HT, ai.ECO_TTC, ai.ON_WEB,
|
||||||
|
ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
||||||
|
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||||
|
ai.PV_CONSEILLE, ai.FRAISLOGISTIC,
|
||||||
|
ai.COMMENTAIRE,
|
||||||
|
n.CODE AS NOM_CODE, n.LIBELLE AS NOM_LIBELLE, n.NIVEAU AS NOM_NIVEAU,
|
||||||
|
n.CHEMIN_PERE AS NOM_CHEMIN_PERE
|
||||||
|
FROM ARTICLES a
|
||||||
|
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||||
|
LEFT JOIN NOMENCLATURE n ON n.NO_ID = a.NOM_NO_ID
|
||||||
|
WHERE a.NO_ID = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
if (!article.rows.length)
|
||||||
|
return res.status(404).json({ error: 'Article introuvable' });
|
||||||
|
|
||||||
|
const gtins = await pool.query(
|
||||||
|
`SELECT GTIN, PREFERENTIEL FROM ART_GTIN WHERE IDARTICLE = $1 ORDER BY PREFERENTIEL DESC`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const gammes = await pool.query(`
|
||||||
|
SELECT g.CODE AS GAMME_CODE, g.LIBELLE AS GAMME_LIBELLE,
|
||||||
|
s.CODE AS SAISON_CODE, s.LIBELLE AS SAISON_LIBELLE,
|
||||||
|
ags.IDGAMME, ags.IDSAISON
|
||||||
|
FROM ART_GAMME_SAISON ags
|
||||||
|
JOIN GAMMES g ON g.NO_ID = ags.IDGAMME
|
||||||
|
LEFT JOIN SAISONS s ON s.NO_ID = ags.IDSAISON
|
||||||
|
WHERE ags.ARTNOID = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const stock = await pool.query(`
|
||||||
|
SELECT Site, QTE, Prmp, ValStock, PV, StockDispo,
|
||||||
|
DerniereVente, DerniereReception, StockMort
|
||||||
|
FROM Cube_Stock WHERE ArtNoId = $1 ORDER BY Site
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const prixAchat = await pool.query(
|
||||||
|
`SELECT ArtNoId, PA FROM Cube_PA WHERE ArtNoId = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const prixVente = await pool.query(
|
||||||
|
`SELECT Site, PV FROM Cube_PV WHERE ArtNoId = $1 ORDER BY Site`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fournisseurs = await pool.query(`
|
||||||
|
SELECT f.CODE AS CODEFOU, fa.RAISONSOCIALE AS NOM_FOU,
|
||||||
|
f.REFERENCE AS REF_FOU, f.EAN13, f.ITF,
|
||||||
|
f.QTEUA AS PCB, f.SPCB, f.DELAI, f.SECURITE,
|
||||||
|
f.PREFERENCE, f.SUSPENDU,
|
||||||
|
t.PRIXACHAT, t.REMISE_PROMOTION
|
||||||
|
FROM ARTFOU1 f
|
||||||
|
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||||
|
LEFT JOIN ARTFOU2 t ON t.IDARTFOU1 = f.NO_ID
|
||||||
|
WHERE f.ART_NO_ID = $1
|
||||||
|
ORDER BY f.PREFERENCE
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const mvtAgreges = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
MAX(CASE WHEN GenreMvt = 1 THEN DatMvt END) AS DERNIERE_ENTREE,
|
||||||
|
MAX(CASE WHEN GenreMvt = 3 THEN DatMvt END) AS DERNIERE_VENTE,
|
||||||
|
SUM(CASE WHEN GenreMvt = 3 THEN QteMvt ELSE 0 END) AS QTE_TOTALE_VENDUE,
|
||||||
|
SUM(CASE WHEN GenreMvt = 3 THEN MntMvtTTC ELSE 0 END) AS CA_TTC_TOTAL,
|
||||||
|
SUM(CASE WHEN GenreMvt = 3 THEN MargeMvt ELSE 0 END) AS MARGE_TOTALE
|
||||||
|
FROM MvtArt WHERE ArtNoId = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
let stockRemballe = [];
|
||||||
|
try {
|
||||||
|
const remb = await pool.query(
|
||||||
|
`SELECT Site, QTE AS QTE_REMBALLE, DateSaisie FROM InvRemballe WHERE ArtNoId = $1 ORDER BY Site`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
stockRemballe = remb.rows;
|
||||||
|
} catch (e) { /* Table optionnelle */ }
|
||||||
|
|
||||||
|
let notes = [];
|
||||||
|
try {
|
||||||
|
const notesRes = await pool.query(
|
||||||
|
`SELECT Note, DateSaisie, Auteur FROM NoteArticles WHERE ArtNoId = $1 ORDER BY DateSaisie DESC`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
notes = notesRes.rows;
|
||||||
|
} catch (e) { /* Table optionnelle */ }
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
article: article.rows[0],
|
||||||
|
gtins: gtins.rows,
|
||||||
|
gammes: gammes.rows,
|
||||||
|
stock: stock.rows,
|
||||||
|
stock_remballe: stockRemballe,
|
||||||
|
prix: {
|
||||||
|
achat: prixAchat.rows[0]?.pa ?? null,
|
||||||
|
vente_par_site: prixVente.rows,
|
||||||
|
},
|
||||||
|
fournisseurs: fournisseurs.rows,
|
||||||
|
performance: mvtAgreges.rows[0],
|
||||||
|
notes,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/articles/:id/mouvements?dateDebut=&dateFin=&site=&page=&limit=
|
||||||
|
router.get('/:id/mouvements', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '', dateFin = '', site = '', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT DatMvt, Site, LibMvt, GenreMvt, QteMvt, ValMvt,
|
||||||
|
MntMvtHt, MntMvtTTC, MargeMvt, QteStock, Prmp, CODEFOU
|
||||||
|
FROM MvtArt
|
||||||
|
WHERE ArtNoId = $1
|
||||||
|
AND DatMvt BETWEEN $2 AND $3
|
||||||
|
AND Site LIKE $4
|
||||||
|
ORDER BY DatMvt DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [req.params.id, dateDebut || '2000-01-01', dateFin || '2099-12-31', `%${site}%`]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, mouvements: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/commandes?dateDebut=&dateFin=&codefou=&page=&limit=
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', codefou = '', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||||
|
f.CODE AS CODEFOU,
|
||||||
|
cv.ARTICLES_CODEIN AS CODEIN,
|
||||||
|
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||||
|
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||||
|
cv.ARTFOU1_EAN13 AS EAN13,
|
||||||
|
cv.ARTFOU1_ITF AS ITF,
|
||||||
|
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||||
|
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||||
|
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXVENTE AS PRIX_VENTE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEREL AS QTE_RELIQUAT,
|
||||||
|
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON_CIBLE,
|
||||||
|
cv.CDEFOU_LIGNE_CDELIGTOT AS DATE_LIVRAISON_TOT,
|
||||||
|
cv.COMMENTAIRE,
|
||||||
|
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||||
|
FROM CDEFOU_VIVANT cv
|
||||||
|
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||||
|
WHERE cv.SUIVIDATECREATION BETWEEN $1 AND $2
|
||||||
|
AND f.CODE LIKE $3
|
||||||
|
ORDER BY cv.SUIVIDATECREATION DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [dateDebut, dateFin, `%${codefou}%`]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, commandes: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/commandes/:noCommande - Détail d'une commande
|
||||||
|
router.get('/:noCommande', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||||
|
f.CODE AS CODEFOU,
|
||||||
|
cv.ARTICLES_CODEIN AS CODEIN,
|
||||||
|
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||||
|
cv.ARTICLES_LIBELLE2 AS LIBELLE2,
|
||||||
|
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||||
|
cv.ARTFOU1_EAN13 AS EAN13,
|
||||||
|
cv.ARTFOU1_ITF AS ITF,
|
||||||
|
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||||
|
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||||
|
cv.CDEFOU_LIGNE_REMISE2 AS REMISE2,
|
||||||
|
cv.CDEFOU_LIGNE_REMISE3 AS REMISE3,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||||
|
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXVENTE AS PRIX_VENTE,
|
||||||
|
cv.CDEFOU_LIGNE_GRATUIT AS QTE_GRATUITE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEREL AS QTE_RELIQUAT,
|
||||||
|
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON_CIBLE,
|
||||||
|
cv.COTISATION_LOGISTIQUE,
|
||||||
|
cv.FRAISLOGISTIC, cv.TRANSIT, cv.DISTRIBUTION, cv.TAXE,
|
||||||
|
cv.COMMENTAIRE,
|
||||||
|
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||||
|
FROM CDEFOU_VIVANT cv
|
||||||
|
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||||
|
WHERE cv.CDEFOU_LIGNE_COM_NO_ID = $1
|
||||||
|
ORDER BY cv.ARTICLES_LIBELLE1
|
||||||
|
`, [req.params.noCommande]);
|
||||||
|
|
||||||
|
if (!result.rows.length)
|
||||||
|
return res.status(404).json({ error: 'Commande introuvable' });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
noCommande: req.params.noCommande,
|
||||||
|
nbLignes: result.rows.length,
|
||||||
|
montantTotal: result.rows.reduce((s, l) => s + (l.montant || 0), 0),
|
||||||
|
lignes: result.rows,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/commandes/receptions/liste?dateDebut=&dateFin=&page=&limit=
|
||||||
|
router.get('/receptions/liste', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
rl.NO_ID, rl.CDEFOU_RECEPTION_NO_ID,
|
||||||
|
rl.QTEBL, rl.QTEREC, rl.QTEACC, rl.QTEREF, rl.QTEAFF,
|
||||||
|
rl.PRIBRUT, rl.REMISE, rl.PRIREC, rl.MNTREC,
|
||||||
|
rl.MOTIFREFUS, rl.RECPB, rl.RECPBOK,
|
||||||
|
f.CODE AS CODEFOU,
|
||||||
|
f.REFERENCE AS REF_FOU, f.EAN13,
|
||||||
|
a.CODEIN, a.LIBELLE1,
|
||||||
|
rl.SUIVIDATECREATION AS DATE_RECEPTION
|
||||||
|
FROM CDEFOU_RECEPLIG rl
|
||||||
|
JOIN ARTFOU1 f ON f.NO_ID = rl.ARTFOU1_NO_ID
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = f.ART_NO_ID
|
||||||
|
WHERE rl.SUIVIDATECREATION BETWEEN $1 AND $2
|
||||||
|
ORDER BY rl.SUIVIDATECREATION DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [dateDebut, dateFin]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, receptions: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/fournisseurs - Liste des fournisseurs distincts
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { search = '' } = req.query;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
f.CODE AS CODEFOU,
|
||||||
|
MAX(fa.RAISONSOCIALE) AS NOM,
|
||||||
|
MAX(fa.ADRLIGNE1) AS ADRESSE,
|
||||||
|
MAX(fa.TELEPHONE) AS TELEPHONE,
|
||||||
|
MAX(fa.EMAIL) AS EMAIL,
|
||||||
|
COUNT(DISTINCT f.ART_NO_ID) AS NB_ARTICLES,
|
||||||
|
MIN(f.SUIVIDATECREATION) AS PREMIERE_ENTREE,
|
||||||
|
MAX(f.SUIVIDATEMODIF) AS DERNIERE_MODIF,
|
||||||
|
SUM(CASE WHEN f.SUSPENDU IS NULL THEN 1 ELSE 0 END) AS NB_ACTIFS,
|
||||||
|
SUM(CASE WHEN f.SUSPENDU IS NOT NULL THEN 1 ELSE 0 END) AS NB_SUSPENDUS
|
||||||
|
FROM ARTFOU1 f
|
||||||
|
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||||
|
WHERE f.CODE LIKE $1
|
||||||
|
GROUP BY f.CODE
|
||||||
|
ORDER BY NB_ARTICLES DESC
|
||||||
|
`, [`%${search}%`]);
|
||||||
|
|
||||||
|
res.json({ count: result.rows.length, fournisseurs: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/fournisseurs/:code/articles?page=&limit=&suspendu=
|
||||||
|
router.get('/:code/articles', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { 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 offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2,
|
||||||
|
f.REFERENCE AS REF_FOU, f.EAN13, f.ITF,
|
||||||
|
f.PCB, f.SPCB, f.DELAI, f.SECURITE, f.SUSPENDU, f.PREFERENCE,
|
||||||
|
t2.PRIXACHAT, t2.REMISE_PROMOTION,
|
||||||
|
pa.PA,
|
||||||
|
pv.PV AS PV_CENTRAL
|
||||||
|
FROM ARTFOU1 f
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = f.ART_NO_ID
|
||||||
|
LEFT JOIN ARTFOU2 t2 ON t2.IDARTFOU1 = f.NO_ID
|
||||||
|
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||||
|
LEFT JOIN Cube_PV pv ON pv.ArtNoId = a.NO_ID AND pv.Site = '000'
|
||||||
|
WHERE f.CODE = $1
|
||||||
|
ORDER BY a.LIBELLE1
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [req.params.code]);
|
||||||
|
|
||||||
|
res.json({ code: req.params.code, page: pageNum, limit: limitNum, articles: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/fournisseurs/:code/commandes?dateDebut=&dateFin=&page=&limit=
|
||||||
|
router.get('/:code/commandes', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||||
|
cv.ARTICLES_CODEIN AS CODEIN,
|
||||||
|
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||||
|
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||||
|
cv.ARTFOU1_EAN13 AS EAN13,
|
||||||
|
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||||
|
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||||
|
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||||
|
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||||
|
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||||
|
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||||
|
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON,
|
||||||
|
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||||
|
FROM CDEFOU_VIVANT cv
|
||||||
|
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||||
|
WHERE f.CODE = $1
|
||||||
|
AND cv.SUIVIDATECREATION BETWEEN $2 AND $3
|
||||||
|
ORDER BY cv.SUIVIDATECREATION DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [req.params.code, dateDebut, dateFin]);
|
||||||
|
|
||||||
|
res.json({ code: req.params.code, page: pageNum, limit: limitNum, commandes: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/mouvements/articles?dateDebut=&dateFin=&site=&codefou=&page=&limit=
|
||||||
|
router.get('/articles', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', codefou = '', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
m.DatMvt, m.Site,
|
||||||
|
a.CODEIN, a.LIBELLE1,
|
||||||
|
m.LibMvt, m.GenreMvt, m.QteMvt, m.ValMvt,
|
||||||
|
m.MntMvtHt, m.MntMvtTTC, m.MargeMvt,
|
||||||
|
m.QteStock, m.Prmp, m.ValStock, m.CODEFOU
|
||||||
|
FROM MvtArt m
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||||
|
WHERE m.DatMvt BETWEEN $1 AND $2
|
||||||
|
AND m.Site LIKE $3
|
||||||
|
AND m.CODEFOU LIKE $4
|
||||||
|
ORDER BY m.DatMvt DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`, `%${codefou}%`]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, mouvements: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/mouvements/reglements?dateDebut=&dateFin=&page=&limit=
|
||||||
|
router.get('/reglements', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
DatMvt, CodTick, CodCarteCli, CodDev,
|
||||||
|
MntReg, MntRegDev, ClientNom, Echeance,
|
||||||
|
REFERENCE, TYPEREG, SuiviDateCreation
|
||||||
|
FROM MvtReg
|
||||||
|
WHERE DatMvt BETWEEN $1 AND $2
|
||||||
|
ORDER BY DatMvt DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [dateDebut, dateFin]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, reglements: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/mouvements/synthese?dateDebut=&dateFin=&site= - Synthèse par jour/site
|
||||||
|
router.get('/synthese', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '' } = req.query;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
DatMvt::DATE AS Jour,
|
||||||
|
Site,
|
||||||
|
GenreMvt,
|
||||||
|
COUNT(*) AS NB_MVT,
|
||||||
|
SUM(QteMvt) AS QTE_TOTALE,
|
||||||
|
SUM(MntMvtHt) AS MNT_HT,
|
||||||
|
SUM(MntMvtTTC) AS MNT_TTC,
|
||||||
|
SUM(MargeMvt) AS MARGE
|
||||||
|
FROM MvtArt
|
||||||
|
WHERE DatMvt BETWEEN $1 AND $2
|
||||||
|
AND Site LIKE $3
|
||||||
|
GROUP BY DatMvt::DATE, Site, GenreMvt
|
||||||
|
ORDER BY Jour DESC, Site
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`]);
|
||||||
|
|
||||||
|
res.json({ synthese: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/performance/ca?dateDebut=&dateFin=&site=&groupBy=jour|mois
|
||||||
|
router.get('/ca', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', groupBy = 'jour' } = req.query;
|
||||||
|
|
||||||
|
const groupExpr = groupBy === 'mois'
|
||||||
|
? "TO_CHAR(DatMvt, 'YYYY-MM')"
|
||||||
|
: "DatMvt::DATE";
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
${groupExpr} AS Periode,
|
||||||
|
Site,
|
||||||
|
COUNT(*) AS NB_VENTES,
|
||||||
|
SUM(QteMvt) AS QTE_VENDUE,
|
||||||
|
SUM(MntMvtHt) AS CA_HT,
|
||||||
|
SUM(MntMvtTTC) AS CA_TTC,
|
||||||
|
SUM(MargeMvt) AS MARGE,
|
||||||
|
CASE WHEN SUM(MntMvtTTC) > 0
|
||||||
|
THEN ROUND(SUM(MargeMvt) / SUM(MntMvtTTC) * 100, 2)
|
||||||
|
ELSE 0 END AS TAUX_MARGE
|
||||||
|
FROM MvtArt
|
||||||
|
WHERE GenreMvt = 3
|
||||||
|
AND DatMvt BETWEEN $1 AND $2
|
||||||
|
AND Site LIKE $3
|
||||||
|
GROUP BY ${groupExpr}, Site
|
||||||
|
ORDER BY Periode DESC, Site
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
groupBy,
|
||||||
|
dateDebut,
|
||||||
|
dateFin,
|
||||||
|
site: site || 'tous',
|
||||||
|
ca: result.rows,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/performance/hitparade?dateDebut=&dateFin=&site=&limit=&groupBy=qte|ca|marge
|
||||||
|
router.get('/hitparade', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', limit = 50, groupBy = 'ca' } = req.query;
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||||
|
|
||||||
|
const orderCol = groupBy === 'qte' ? 'QTE_VENDUE' : groupBy === 'marge' ? 'MARGE' : 'CA_TTC';
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.CODEIN,
|
||||||
|
a.LIBELLE1,
|
||||||
|
m.Site,
|
||||||
|
COUNT(*) AS NB_PASSAGES,
|
||||||
|
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||||
|
SUM(m.MntMvtHt) AS CA_HT,
|
||||||
|
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||||
|
SUM(m.MargeMvt) AS MARGE,
|
||||||
|
CASE WHEN SUM(m.MntMvtTTC) > 0
|
||||||
|
THEN ROUND(SUM(m.MargeMvt) / SUM(m.MntMvtTTC) * 100, 2)
|
||||||
|
ELSE 0 END AS TAUX_MARGE,
|
||||||
|
MAX(m.DatMvt) AS DERNIERE_VENTE
|
||||||
|
FROM MvtArt m
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||||
|
WHERE m.GenreMvt = 3
|
||||||
|
AND m.DatMvt BETWEEN $1 AND $2
|
||||||
|
AND m.Site LIKE $3
|
||||||
|
GROUP BY a.CODEIN, a.LIBELLE1, m.Site
|
||||||
|
ORDER BY ${orderCol} DESC
|
||||||
|
LIMIT ${limitNum}
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
dateDebut,
|
||||||
|
dateFin,
|
||||||
|
site: site || 'tous',
|
||||||
|
classementPar: groupBy,
|
||||||
|
hitparade: result.rows,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/performance/ca/nomenclature?dateDebut=&dateFin=&site=&niveau=
|
||||||
|
router.get('/ca/nomenclature', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', niveau = 1 } = req.query;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
n.CODE AS CODE_NOMEN,
|
||||||
|
n.LIBELLE,
|
||||||
|
n.NIVEAU,
|
||||||
|
COUNT(DISTINCT m.ArtNoId) AS NB_ARTICLES,
|
||||||
|
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||||
|
SUM(m.MntMvtHt) AS CA_HT,
|
||||||
|
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||||
|
SUM(m.MargeMvt) AS MARGE
|
||||||
|
FROM MvtArt m
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||||
|
JOIN NOMENCLATURE n ON n.NO_ID = a.NOM_NO_ID
|
||||||
|
WHERE m.GenreMvt = 3
|
||||||
|
AND m.DatMvt BETWEEN $1 AND $2
|
||||||
|
AND m.Site LIKE $3
|
||||||
|
AND n.NIVEAU = $4
|
||||||
|
GROUP BY n.CODE, n.LIBELLE, n.NIVEAU
|
||||||
|
ORDER BY CA_TTC DESC
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`, parseInt(niveau)]);
|
||||||
|
|
||||||
|
res.json({ dateDebut, dateFin, niveau: parseInt(niveau), ca_par_nomenclature: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/performance/ca/gamme?dateDebut=&dateFin=&site=
|
||||||
|
router.get('/ca/gamme', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '' } = req.query;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
g.CODE AS GAMME,
|
||||||
|
g.LIBELLE AS LIBELLE_GAMME,
|
||||||
|
COUNT(DISTINCT m.ArtNoId) AS NB_ARTICLES,
|
||||||
|
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||||
|
SUM(m.MntMvtHt) AS CA_HT,
|
||||||
|
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||||
|
SUM(m.MargeMvt) AS MARGE
|
||||||
|
FROM MvtArt m
|
||||||
|
JOIN ART_GAMME_SAISON ags ON ags.ARTNOID = m.ArtNoId
|
||||||
|
JOIN GAMMES g ON g.NO_ID = ags.IDGAMME
|
||||||
|
WHERE m.GenreMvt = 3
|
||||||
|
AND m.DatMvt BETWEEN $1 AND $2
|
||||||
|
AND m.Site LIKE $3
|
||||||
|
GROUP BY g.CODE, g.LIBELLE
|
||||||
|
ORDER BY CA_TTC DESC
|
||||||
|
`, [dateDebut, dateFin, `%${site}%`]);
|
||||||
|
|
||||||
|
res.json({ dateDebut, dateFin, ca_par_gamme: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/schema - Structure complète de la base de données
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
table_schema AS TABLE_SCHEMA,
|
||||||
|
table_name AS TABLE_NAME,
|
||||||
|
table_type AS TABLE_TYPE,
|
||||||
|
column_name AS COLUMN_NAME,
|
||||||
|
ordinal_position AS ORDINAL_POSITION,
|
||||||
|
data_type AS DATA_TYPE,
|
||||||
|
character_maximum_length AS CHARACTER_MAXIMUM_LENGTH,
|
||||||
|
numeric_precision AS NUMERIC_PRECISION,
|
||||||
|
numeric_scale AS NUMERIC_SCALE,
|
||||||
|
is_nullable AS IS_NULLABLE,
|
||||||
|
column_default AS COLUMN_DEFAULT
|
||||||
|
FROM information_schema.tables
|
||||||
|
JOIN information_schema.columns USING (table_schema, table_name)
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
ORDER BY table_name, ordinal_position
|
||||||
|
`);
|
||||||
|
|
||||||
|
const schema = {};
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const key = `${row.table_schema}.${row.table_name}`;
|
||||||
|
if (!schema[key]) {
|
||||||
|
schema[key] = {
|
||||||
|
schema: row.table_schema,
|
||||||
|
table: row.table_name,
|
||||||
|
type: row.table_type,
|
||||||
|
columns: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
schema[key].columns.push({
|
||||||
|
name: row.column_name,
|
||||||
|
position: row.ordinal_position,
|
||||||
|
type: row.data_type,
|
||||||
|
maxLength: row.character_maximum_length,
|
||||||
|
precision: row.numeric_precision,
|
||||||
|
scale: row.numeric_scale,
|
||||||
|
nullable: row.is_nullable === 'YES',
|
||||||
|
default: row.column_default,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = Object.values(schema);
|
||||||
|
res.json({
|
||||||
|
database: process.env.PG_DB,
|
||||||
|
server: process.env.PG_HOST,
|
||||||
|
tableCount: tables.filter(t => t.type === 'BASE TABLE').length,
|
||||||
|
viewCount: tables.filter(t => t.type === 'VIEW').length,
|
||||||
|
tables,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/schema/tables?search= - Liste des tables avec nombre de lignes (estimé)
|
||||||
|
router.get('/tables', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const search = req.query.search || '';
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname AS TABLE_SCHEMA,
|
||||||
|
tablename AS TABLE_NAME,
|
||||||
|
'BASE TABLE' AS TABLE_TYPE,
|
||||||
|
n_live_tup AS ROW_COUNT
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE tablename LIKE $1
|
||||||
|
ORDER BY n_live_tup DESC, tablename
|
||||||
|
`, [`%${search}%`]);
|
||||||
|
|
||||||
|
res.json({ count: result.rows.length, tables: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/schema/top?limit= - Top tables par nombre de lignes
|
||||||
|
router.get('/top', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(req.query.limit) || 50, 200));
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT tablename AS TABLE_NAME, 'BASE TABLE' AS TABLE_TYPE, n_live_tup AS ROW_COUNT
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
ORDER BY n_live_tup DESC
|
||||||
|
LIMIT $1
|
||||||
|
`, [limitNum]);
|
||||||
|
|
||||||
|
res.json({ tables: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/schema/tables/:tableName - Colonnes d'une table
|
||||||
|
router.get('/tables/:tableName', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
column_name AS COLUMN_NAME,
|
||||||
|
ordinal_position AS ORDINAL_POSITION,
|
||||||
|
data_type AS DATA_TYPE,
|
||||||
|
character_maximum_length AS CHARACTER_MAXIMUM_LENGTH,
|
||||||
|
numeric_precision AS NUMERIC_PRECISION,
|
||||||
|
numeric_scale AS NUMERIC_SCALE,
|
||||||
|
is_nullable AS IS_NULLABLE,
|
||||||
|
column_default AS COLUMN_DEFAULT
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = $1 AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`, [req.params.tableName]);
|
||||||
|
|
||||||
|
if (!result.rows.length)
|
||||||
|
return res.status(404).json({ error: `Table '${req.params.tableName}' introuvable` });
|
||||||
|
|
||||||
|
res.json({ table: req.params.tableName, columns: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/stock?site=&page=&limit= - Stock global
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { site = '', page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.CODEIN, a.LIBELLE1,
|
||||||
|
s.Site, s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||||
|
s.StockDispo, s.StockMort, s.StockColis,
|
||||||
|
s.DerniereVente, s.DerniereReception, s.PremiereVente,
|
||||||
|
s.NbJoursDernierMouvement, s.NbJoursDerniereVente,
|
||||||
|
s.NbJoursDerniereReception, s.InterditAchat
|
||||||
|
FROM Cube_Stock s
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = s.ArtNoId
|
||||||
|
WHERE s.Site LIKE $1
|
||||||
|
ORDER BY a.LIBELLE1, s.Site
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [`%${site}%`]);
|
||||||
|
|
||||||
|
res.json({ page: pageNum, limit: limitNum, stock: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/stock/article/:id - Stock d'un article par site
|
||||||
|
router.get('/article/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
s.Site, s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||||
|
s.StockDispo, s.StockMort, s.StockColis,
|
||||||
|
s.DerniereVente, s.DerniereReception, s.PremiereVente,
|
||||||
|
s.NbJoursDernierMouvement, s.NbJoursDerniereVente,
|
||||||
|
s.NbJoursDerniereReception, s.InterditAchat, s.CODEFOU
|
||||||
|
FROM Cube_Stock s
|
||||||
|
WHERE s.ArtNoId = $1
|
||||||
|
ORDER BY s.Site
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
res.json({ artNoId: req.params.id, stock: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/stock/site/:site - Tous les articles d'un site
|
||||||
|
router.get('/site/:site', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { page = 1, limit = 100 } = req.query;
|
||||||
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||||
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.NO_ID AS ART_NO_ID, a.CODEIN, a.LIBELLE1,
|
||||||
|
s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||||
|
s.StockDispo, s.StockMort, s.DerniereVente, s.DerniereReception,
|
||||||
|
s.NbJoursDerniereVente, s.NbJoursDerniereReception
|
||||||
|
FROM Cube_Stock s
|
||||||
|
JOIN ARTICLES a ON a.NO_ID = s.ArtNoId
|
||||||
|
WHERE s.Site = $1
|
||||||
|
ORDER BY s.QTE DESC
|
||||||
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
|
`, [req.params.site]);
|
||||||
|
|
||||||
|
res.json({ site: req.params.site, page: pageNum, limit: limitNum, stock: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/stock/valorisation - Valorisation du stock par site
|
||||||
|
router.get('/valorisation', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
Site,
|
||||||
|
COUNT(*) AS NB_ARTICLES,
|
||||||
|
SUM(QTE) AS QTE_TOTALE,
|
||||||
|
SUM(ValStock) AS VAL_STOCK,
|
||||||
|
SUM(QTE * PV) AS VAL_PV_THEORIQUE,
|
||||||
|
AVG(Prmp) AS PRMP_MOYEN
|
||||||
|
FROM Cube_Stock
|
||||||
|
WHERE QTE > 0
|
||||||
|
GROUP BY Site
|
||||||
|
ORDER BY VAL_STOCK DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.json({ valorisation: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/sync/status - Dernier état de chaque table synchronisée
|
||||||
|
router.get('/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT table_name, last_sync, rows_synced, status, error_msg
|
||||||
|
FROM sync_log ORDER BY last_sync DESC NULLS LAST`
|
||||||
|
);
|
||||||
|
res.json({ sync: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -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