Author SHA1 Message Date
MichaelandClaude Opus 4.8 adc8d357c9 fix: dedupe cube_pa rows by artnoid before full refresh
Cube_PA is a heap in SQL Server with no primary key, while the
PostgreSQL cube_pa table uses artnoid as PRIMARY KEY. The cube
normally emits one row per article, but for 3 articles supplied by
two vendors it emitted both vendor prices, so fullRefresh hit:

  duplicate key value violates unique constraint "cube_pa_pkey"

These are not exact duplicates: the two rows carry different PA
values, so the choice changes the article's purchase price. Resolve
by keeping the price whose last change is the most recent, read from
the dated history packed into ARTFOU2.PRIXACHAT
("[01/01/1901,0.990:16/05/2018,1.010]"). SUIVIDATEMODIF cannot be
used alone: it is not touched when only the price changes (S028 shows
modif 2017 but a price change in 2025). It only breaks ties.

The lookup runs solely when duplicates are present, so the nightly
sync is unaffected in the normal case.

Verified against the live 427 752-row Cube_PA: 427 749 rows out, all
artnoid distinct, no fallback triggered. Parsing PRIXACHAT over 20 000
ARTFOU2 rows: 19 999 parsed, 1 empty, 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:30:49 +02:00
Michael 950539472f Merge remote-tracking branch 'origin/claude/gallant-edison-i5bg90' 2026-07-09 09:13:35 +02:00
LogiFlow e01048fe3b Merge pull request #2 from R0m1k3/feat/artcentrale-api
feat(articles): expose and filter code centrale (artcentrale)
2026-07-09 09:10:28 +02:00
Claude c5b0896bb4 fix: dedupe foucad rows by foucode before full refresh
The source MSSQL FOUCAD table can return several rows for the same
FOUCODE, but the PostgreSQL foucad table uses foucode as PRIMARY KEY.
The bulk INSERT in fullRefresh has no conflict handling, so duplicate
FOUCODE values triggered:

  duplicate key value violates unique constraint "foucad_pkey"

Deduplicate by foucode before insert, keeping the most recently
modified row (suividatemodif). Routes join foucad 1:1 on foucode, so
one config row per supplier code is the intended shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ad1qwhDEHNniSUDveTQHL
2026-06-29 06:30:00 +00:00
2 changed files with 86 additions and 2 deletions

No files matched your search

+13 -1
View File
@@ -171,7 +171,19 @@ async function syncAppro(force) {
suividatemodif: r.SUIVIDATEMODIF,
}));
const count = await fullRefresh(pg, 'foucad', rows, FOUCAD_COLS);
// foucode est PK côté PostgreSQL mais la source FOUCAD peut renvoyer
// plusieurs lignes par FOUCODE → on déduplique en gardant la plus récente
// (suividatemodif), sinon l'INSERT viole foucad_pkey.
const byCode = new Map();
for (const row of rows) {
const prev = byCode.get(row.foucode);
if (!prev || (row.suividatemodif ?? 0) >= (prev.suividatemodif ?? 0)) {
byCode.set(row.foucode, row);
}
}
const dedupRows = [...byCode.values()];
const count = await fullRefresh(pg, 'foucad', dedupRows, FOUCAD_COLS);
await logSync(pg, 'foucad', count, 'ok');
console.log(`[foucad] ${count} lignes`);
} catch (err) {
+73 -1
View File
@@ -11,6 +11,48 @@ const STOCK_COLS = [
const PA_COLS = ['artnoid','pa'];
const PV_COLS = ['artnoid','site','pv'];
// ARTFOU2.PRIXACHAT empile l'historique des prix d'un fournisseur sous forme
// "[01/01/1901,0.990:16/05/2018,1.010]". Le dernier couple porte le prix courant
// et la date à laquelle il a été appliqué.
function lastPriceChange(s) {
if (!s) return null;
const body = String(s).trim().replace(/^\[/, '').replace(/\]$/, '');
if (!body) return null;
const pairs = body.split(':');
const [d, p] = pairs[pairs.length - 1].split(',');
const m = d && d.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
const price = parseFloat(p);
if (!m || Number.isNaN(price)) return null;
return { date: Date.UTC(+m[3], +m[2] - 1, +m[1]), price };
}
// Pour chaque artnoid en doublon, retrouve le prix dont le changement est le plus
// récent. Égalité de date de prix → on départage sur SUIVIDATEMODIF de la ligne.
async function resolveDuplicatePa(ms, artnoids) {
const ids = artnoids.map(Number).filter(Number.isFinite);
if (!ids.length) return new Map();
const res = await ms.request().query(`
SELECT f1.ART_NO_ID, f2.PRIXACHAT, f2.SUIVIDATEMODIF
FROM ARTFOU1 f1
JOIN ARTFOU2 f2 ON f2.IDARTFOU1 = f1.NO_ID
WHERE f1.ART_NO_ID IN (${ids.join(',')})
`);
const best = new Map();
for (const r of res.recordset) {
const lp = lastPriceChange(r.PRIXACHAT);
if (!lp) continue;
const key = String(r.ART_NO_ID);
const modif = r.SUIVIDATEMODIF ? new Date(r.SUIVIDATEMODIF).getTime() : 0;
const prev = best.get(key);
if (!prev || lp.date > prev.date || (lp.date === prev.date && modif > prev.modif)) {
best.set(key, { date: lp.date, price: lp.price, modif });
}
}
return best;
}
async function syncStock(force) {
const ms = await getMssql();
const pg = getPg();
@@ -57,7 +99,37 @@ async function syncStock(force) {
// === 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 raw = res.recordset.map(r => ({ artnoid: r.ArtNoId, pa: r.PA }));
// artnoid est PK côté PostgreSQL, mais le cube peut sortir deux lignes pour
// un article livré par deux fournisseurs → l'INSERT violerait cube_pa_pkey.
const groups = new Map();
for (const r of raw) {
const key = String(r.artnoid);
const g = groups.get(key);
if (g) g.push(r); else groups.set(key, [r]);
}
const dupIds = [...groups].filter(([, g]) => g.length > 1).map(([id]) => id);
let rows = raw;
if (dupIds.length) {
const best = await resolveDuplicatePa(ms, dupIds);
rows = [];
for (const [key, g] of groups) {
if (g.length === 1) { rows.push(g[0]); continue; }
const b = best.get(key);
let chosen = b && g.find(x => Math.abs(parseFloat(x.pa) - b.price) < 0.005);
if (!chosen) {
// Pas de prix fournisseur exploitable : on retient le PA le plus élevé,
// qui sous-estime la marge plutôt que de l'inventer.
chosen = g.reduce((a, x) => (parseFloat(x.pa) > parseFloat(a.pa) ? x : a));
console.warn(`[cube_pa] artnoid ${key} non résolu → PA le plus élevé retenu (${chosen.pa})`);
}
rows.push(chosen);
}
console.log(`[cube_pa] ${dupIds.length} artnoid dédupliqués`);
}
const count = await fullRefresh(pg, 'cube_pa', rows, PA_COLS);
await logSync(pg, 'cube_pa', count, 'ok');
console.log(`[cube_pa] ${count} lignes refresh`);