mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 22:18:08 +02:00
feat: switch to /articles/:no_id/mensuel endpoint for sales & stock data
Replaces bulk mouvements pagination + per-article stock N+1 with a single batched call to the new mensuel endpoint (confirmed working via diag). api-ff-client.ts: - Remove getStockByFournisseur (no longer needed) - Add FfMensuelEntry/Ventes/Receptions interfaces - Add getMensuelByArticles(articles, dateDebut, dateFin, batchSize=20) → Map<codein, FfMensuelEntry[]> using no_id as API identifier get-product-rows.ts: - Phase 2: getMensuelByArticles + getCommandesByFournisseur in parallel - Phase 5: aggregate mois×site entries → sales12m, stock12m, workingStores - qte_vendue / ca_ht are negative → Math.abs() - stock carry-forward for months with no data - pa from prmp_fin_mois Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2d37a134d4
commit
58f17152a1
2 files changed
+127
-171
No files matched your search
@@ -4,11 +4,9 @@ import type { ProductRow, GammeCode, GridFilters } from "@/types/grid";
|
|||||||
import { computeProductScores } from "@/lib/score-engine";
|
import { computeProductScores } from "@/lib/score-engine";
|
||||||
import {
|
import {
|
||||||
getArticlesByFournisseur,
|
getArticlesByFournisseur,
|
||||||
getMouvementsByFournisseur,
|
getMensuelByArticles,
|
||||||
getStockByFournisseur,
|
|
||||||
getCommandesByFournisseur,
|
getCommandesByFournisseur,
|
||||||
buildLast12MonthsRange,
|
buildLast12MonthsRange,
|
||||||
dateToYYYYMM,
|
|
||||||
} from "@/lib/api-ff-client";
|
} from "@/lib/api-ff-client";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { sessionSnapshots } from "@/db/schema";
|
import { sessionSnapshots } from "@/db/schema";
|
||||||
@@ -22,39 +20,32 @@ interface GetProductRowsInput {
|
|||||||
|
|
||||||
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
|
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
|
||||||
const { codeFournisseur, magasin = "TOTAL" } = input;
|
const { codeFournisseur, magasin = "TOTAL" } = input;
|
||||||
console.log(`\n>>> [getProductRows] RECEIVED CALL for supplier: ${codeFournisseur}, magasin: ${magasin}`);
|
console.log(`\n>>> [getProductRows] supplier: ${codeFournisseur}, magasin: ${magasin}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ─── Phase 1 : Articles du fournisseur ───────────────────────────────
|
// ─── Phase 1 : Articles du fournisseur ───────────────────────────────
|
||||||
const articles = await getArticlesByFournisseur(codeFournisseur);
|
const articles = await getArticlesByFournisseur(codeFournisseur);
|
||||||
console.log(`[getProductRows] ${articles.length} articles found for ${codeFournisseur}`);
|
console.log(`[getProductRows] ${articles.length} articles for ${codeFournisseur}`);
|
||||||
|
|
||||||
// ─── Phase 2 : Mouvements (ventes + stock12m) ────────────────────────
|
// ─── Phase 2 : Mensuel + Commandes (en parallèle) ────────────────────
|
||||||
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
||||||
const mouvements = await getMouvementsByFournisseur(codeFournisseur, dateDebut, dateFin);
|
const [mensuelMap, commandesMap] = await Promise.all([
|
||||||
console.log(`[getProductRows] ${mouvements.length} mouvements found`);
|
getMensuelByArticles(articles, dateDebut, dateFin),
|
||||||
|
|
||||||
// ─── Phase 3 : Stock & Commandes (en parallèle) ──────────────────────
|
|
||||||
const [stockMap, commandesMap] = await Promise.all([
|
|
||||||
getStockByFournisseur(articles),
|
|
||||||
getCommandesByFournisseur(codeFournisseur),
|
getCommandesByFournisseur(codeFournisseur),
|
||||||
]);
|
]);
|
||||||
|
console.log(`[getProductRows] mensuel data for ${mensuelMap.size}/${articles.length} articles`);
|
||||||
|
|
||||||
// ─── Phase 4 : Fenêtre temporelle (12 mois complets) ─────────────────
|
// ─── Phase 3 : Fenêtre temporelle (12 mois complets) ─────────────────
|
||||||
// En mars 2026 → allowedPeriods = {"202503"..."202602"} (12 mois)
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const allowedPeriods = new Set<string>();
|
const allowedPeriods = new Set<string>();
|
||||||
for (let i = 12; i >= 1; i--) {
|
for (let i = 12; i >= 1; i--) {
|
||||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
allowedPeriods.add(`${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`);
|
allowedPeriods.add(`${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`);
|
||||||
}
|
}
|
||||||
|
const sortedPeriods = [...allowedPeriods].sort(); // "202503" … "202602"
|
||||||
|
|
||||||
// ─── Phase 5 : Seed productMap depuis articles ────────────────────────
|
// ─── Phase 4 : Seed productMap depuis articles ────────────────────────
|
||||||
const productMap = new Map<string, ProductRow>();
|
const productMap = new Map<string, ProductRow>();
|
||||||
const storeParticipationMap = new Map<string, Map<string, Set<string>>>();
|
|
||||||
// stockMaxByPeriod[codein][YYYYMM] = dernier qtestock du mois
|
|
||||||
const stockLastByPeriod = new Map<string, Map<string, { qtestock: number; datemvt: string }>>();
|
|
||||||
|
|
||||||
for (const art of articles) {
|
for (const art of articles) {
|
||||||
if (!productMap.has(art.codein)) {
|
if (!productMap.has(art.codein)) {
|
||||||
productMap.set(art.codein, {
|
productMap.set(art.codein, {
|
||||||
@@ -64,12 +55,9 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
libelle1: art.libelle1 ?? "",
|
libelle1: art.libelle1 ?? "",
|
||||||
gtin: "",
|
gtin: "",
|
||||||
reference: "",
|
reference: "",
|
||||||
code1: "",
|
code1: "", libelleNiveau1: "",
|
||||||
libelleNiveau1: "",
|
code2: "", libelleNiveau2: "",
|
||||||
code2: "",
|
code3: "", libelle3: "",
|
||||||
libelleNiveau2: "",
|
|
||||||
code3: "",
|
|
||||||
libelle3: "",
|
|
||||||
codeGamme: null,
|
codeGamme: null,
|
||||||
codeGammeInit: null,
|
codeGammeInit: null,
|
||||||
codeGammeDraft: null,
|
codeGammeDraft: null,
|
||||||
@@ -82,138 +70,118 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
score: 0,
|
score: 0,
|
||||||
workingStores: [],
|
workingStores: [],
|
||||||
aiRecommendation: null,
|
aiRecommendation: null,
|
||||||
pcb: art.pcb,
|
pcb: art.pcb ? Number(art.pcb) : undefined,
|
||||||
prixVente: art.pv_central,
|
prixVente: art.pv_central ? Number(art.pv_central) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Phase 6 : Agréger mouvements → sales12m + stock12m ─────────────
|
// ─── Phase 5 : Agréger données mensuelles → sales12m + stock12m ──────
|
||||||
for (const mvt of mouvements) {
|
const filterSite = magasin !== "TOTAL" ? magasin : null;
|
||||||
const { codein, genremvt, datemvt, qte, montant, marge, qtestock, site } = mvt;
|
|
||||||
|
|
||||||
// Filtre magasin
|
|
||||||
if (magasin !== "TOTAL" && site !== magasin) continue;
|
|
||||||
|
|
||||||
const periode = dateToYYYYMM(datemvt);
|
|
||||||
const isInPeriod = allowedPeriods.has(periode);
|
|
||||||
|
|
||||||
|
for (const [codein, entries] of mensuelMap.entries()) {
|
||||||
const product = productMap.get(codein);
|
const product = productMap.get(codein);
|
||||||
if (!product) continue;
|
if (!product) continue;
|
||||||
|
|
||||||
// Participation magasin (pour workingStores)
|
// Agréger par période (toutes les entrées site × mois → 1 entrée par mois)
|
||||||
if (isInPeriod && site && site !== "TOTAL") {
|
const byPeriod = new Map<string, { qty: number; ca: number; marge: number; stock: number; pa: number }>();
|
||||||
if (!storeParticipationMap.has(codein)) storeParticipationMap.set(codein, new Map());
|
const storeMonths = new Map<string, Set<string>>(); // site → Set<YYYYMM>
|
||||||
const stores = storeParticipationMap.get(codein)!;
|
|
||||||
if (!stores.has(site)) stores.set(site, new Set());
|
|
||||||
stores.get(site)!.add(periode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ventes (genremvt = 3)
|
for (const entry of entries) {
|
||||||
if (isInPeriod && genremvt === 3) {
|
if (filterSite && entry.site !== filterSite) continue;
|
||||||
const qty = Math.abs(qte ?? 0);
|
|
||||||
const ca = Math.abs(montant ?? 0);
|
|
||||||
const mg = Math.abs(marge ?? 0);
|
|
||||||
product.sales12m[periode] = (product.sales12m[periode] ?? 0) + qty;
|
|
||||||
product.totalQuantite += qty;
|
|
||||||
product.totalCa += ca;
|
|
||||||
product.totalMarge += mg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stock12m : conserver le dernier mouvement par mois (via qtestock)
|
const periode = entry.mois.replace("-", ""); // "2026-02" → "202602"
|
||||||
// On inclut tous les mouvements (pas que les ventes) pour avoir le stock fin de mois
|
if (!allowedPeriods.has(periode)) continue;
|
||||||
if (isInPeriod) {
|
|
||||||
if (!stockLastByPeriod.has(codein)) stockLastByPeriod.set(codein, new Map());
|
if (!byPeriod.has(periode)) {
|
||||||
const byPeriod = stockLastByPeriod.get(codein)!;
|
byPeriod.set(periode, { qty: 0, ca: 0, marge: 0, stock: 0, pa: 0 });
|
||||||
const existing = byPeriod.get(periode);
|
|
||||||
if (!existing || datemvt >= existing.datemvt) {
|
|
||||||
byPeriod.set(periode, { qtestock, datemvt });
|
|
||||||
}
|
}
|
||||||
}
|
const p = byPeriod.get(periode)!;
|
||||||
}
|
|
||||||
|
|
||||||
// Calcul tauxMarge + reconstruction stock12m avec carry-forward
|
// Stock fin de mois : somme tous sites (ou site filtré)
|
||||||
for (const [codein, product] of productMap.entries()) {
|
p.stock += Math.abs(parseFloat(entry.stock_fin_mois ?? "0") || 0);
|
||||||
product.tauxMarge = product.totalCa > 0
|
if (!p.pa && entry.prmp_fin_mois) p.pa = parseFloat(entry.prmp_fin_mois) || 0;
|
||||||
? (product.totalMarge / product.totalCa) * 100
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Reconstruction stock12m avec carry-forward
|
// Ventes
|
||||||
const byPeriod = stockLastByPeriod.get(codein);
|
if (entry.ventes) {
|
||||||
if (byPeriod) {
|
p.qty += Math.abs(parseFloat(entry.ventes.qte_vendue ?? "0") || 0);
|
||||||
const sortedPeriods = [...allowedPeriods].sort();
|
p.ca += Math.abs(parseFloat(entry.ventes.ca_ht ?? "0") || 0);
|
||||||
let lastKnownStock = 0;
|
p.marge += parseFloat(entry.ventes.marge ?? "0") || 0;
|
||||||
for (const p of sortedPeriods) {
|
|
||||||
const entry = byPeriod.get(p);
|
// Suivi magasins actifs (pour workingStores)
|
||||||
if (entry !== undefined) {
|
if (entry.site && entry.site !== "TOTAL") {
|
||||||
lastKnownStock = entry.qtestock;
|
if (!storeMonths.has(entry.site)) storeMonths.set(entry.site, new Set());
|
||||||
|
storeMonths.get(entry.site)!.add(periode);
|
||||||
}
|
}
|
||||||
product.stock12m[p] = lastKnownStock;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Phase 7 : workingStores ─────────────────────────────────────────
|
// Remplir sales12m / stock12m avec carry-forward sur les mois sans données
|
||||||
for (const [codein, product] of productMap.entries()) {
|
let lastStock = 0;
|
||||||
const stores = storeParticipationMap.get(codein);
|
let lastPa = 0;
|
||||||
if (stores) {
|
for (const periode of sortedPeriods) {
|
||||||
const working: string[] = [];
|
const p = byPeriod.get(periode);
|
||||||
for (const [site, periods] of stores.entries()) {
|
if (p) {
|
||||||
if (site !== "TOTAL" && periods.size >= 3) working.push(site);
|
product.sales12m[periode] = p.qty;
|
||||||
|
product.stock12m[periode] = p.stock;
|
||||||
|
product.totalQuantite += p.qty;
|
||||||
|
product.totalCa += p.ca;
|
||||||
|
product.totalMarge += p.marge;
|
||||||
|
lastStock = p.stock;
|
||||||
|
if (!lastPa && p.pa) lastPa = p.pa;
|
||||||
|
} else {
|
||||||
|
product.sales12m[periode] = 0;
|
||||||
|
product.stock12m[periode] = lastStock; // carry-forward
|
||||||
}
|
}
|
||||||
product.workingStores = working.sort();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Phase 8 : Enrichir avec stockMap ────────────────────────────────
|
product.tauxMarge = product.totalCa > 0 ? (product.totalMarge / product.totalCa) * 100 : 0;
|
||||||
for (const [codein, product] of productMap.entries()) {
|
product.pa = lastPa > 0 ? lastPa : undefined;
|
||||||
const stock = stockMap.get(codein);
|
product.prixAchat = lastPa > 0 ? lastPa : undefined;
|
||||||
if (stock) {
|
product.stockActuel = product.stock12m[sortedPeriods[sortedPeriods.length - 1]] ?? 0;
|
||||||
product.stockActuel = stock.stockActuel;
|
|
||||||
product.stockTotal = stock.stockTotal;
|
// workingStores : sites avec ventes sur >= 3 mois distincts
|
||||||
product.stockValeur = stock.stockValeur;
|
product.workingStores = [...storeMonths.entries()]
|
||||||
product.pa = stock.pa;
|
.filter(([, periods]) => periods.size >= 3)
|
||||||
product.prixAchat = stock.pa;
|
.map(([site]) => site)
|
||||||
product.derniereVente = stock.derniereVente || undefined;
|
.sort();
|
||||||
product.derniereLivraison = stock.derniereLivraison || undefined;
|
|
||||||
product.nbJoursDerniereVente = stock.nbJoursDerniereVente;
|
// Commandes en cours
|
||||||
}
|
|
||||||
const cmdQty = commandesMap.get(codein);
|
const cmdQty = commandesMap.get(codein);
|
||||||
if (cmdQty) product.commandesEnCours = cmdQty;
|
if (cmdQty) product.commandesEnCours = cmdQty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Phase 8b : Restaurer gammes depuis dernier snapshot ─────────────
|
// ─── Phase 6 : Restaurer gammes depuis dernier snapshot ──────────────
|
||||||
try {
|
try {
|
||||||
const snapshots = await db
|
const snaps = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sessionSnapshots)
|
.from(sessionSnapshots)
|
||||||
.where(eq(sessionSnapshots.codeFournisseur, codeFournisseur))
|
.where(eq(sessionSnapshots.codeFournisseur, codeFournisseur))
|
||||||
.orderBy(desc(sessionSnapshots.createdAt))
|
.orderBy(desc(sessionSnapshots.createdAt))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (snapshots.length > 0) {
|
if (snaps.length > 0) {
|
||||||
const snap = snapshots[0];
|
const changes = snaps[0].changes as Record<string, { before: string | null; after: string }>;
|
||||||
const changes = snap.changes as Record<string, { before: string | null; after: string }>;
|
|
||||||
for (const [codein, change] of Object.entries(changes)) {
|
for (const [codein, change] of Object.entries(changes)) {
|
||||||
const product = productMap.get(codein);
|
const product = productMap.get(codein);
|
||||||
if (product && change.after) {
|
if (product && change.after) {
|
||||||
product.codeGamme = change.after as GammeCode;
|
product.codeGamme = change.after as GammeCode;
|
||||||
product.codeGammeInit = change.before as GammeCode | null;
|
product.codeGammeInit = change.before as GammeCode | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[getProductRows] Restored ${Object.keys(changes).length} gammes from snapshot ${snap.id}`);
|
console.log(`[getProductRows] Gammes restaurées depuis snapshot`);
|
||||||
}
|
}
|
||||||
} catch (snapErr) {
|
} catch (snapErr) {
|
||||||
console.error("[getProductRows] Failed to restore gammes from snapshot:", snapErr);
|
console.error("[getProductRows] Snapshot restore error:", snapErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Phase 9 : Score composite ────────────────────────────────────────
|
// ─── Phase 7 : Score composite ────────────────────────────────────────
|
||||||
const rows = Array.from(productMap.values());
|
const rows = Array.from(productMap.values());
|
||||||
console.log(`[getProductRows] Final: ${rows.length} products`);
|
console.log(`[getProductRows] ${rows.length} produits, ${mensuelMap.size} avec données`);
|
||||||
return computeProductScores(rows);
|
return computeProductScores(rows);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[getProductRows] Error for supplier ${codeFournisseur}:`, error);
|
console.error(`[getProductRows] Error for ${codeFournisseur}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+47
-59
@@ -242,75 +242,63 @@ export async function getMouvementsByFournisseur(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Stock (agrégé tous sites)
|
// Mensuel — stock fin de mois + ventes + réceptions par article et par site
|
||||||
|
// GET /api/articles/:no_id/mensuel?dateDebut=&dateFin=
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export async function getStockByFournisseur(
|
export interface FfMensuelVentes {
|
||||||
articles: FfArticle[]
|
nb_passages: string;
|
||||||
): Promise<Map<string, AggregatedStock>> {
|
qte_vendue: string; // négatif (ex: "-2.000")
|
||||||
const result = new Map<string, AggregatedStock>();
|
ca_ht: string; // négatif (ex: "-26.6600")
|
||||||
const BATCH_SIZE = 10;
|
ca_ttc: string;
|
||||||
|
marge: string; // positif (ex: "12.7600")
|
||||||
|
taux_marge: string;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < articles.length; i += BATCH_SIZE) {
|
export interface FfMensuelReceptions {
|
||||||
const batch = articles.slice(i, i + BATCH_SIZE);
|
nb_receptions: string;
|
||||||
|
qte_recue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FfMensuelEntry {
|
||||||
|
mois: string; // "YYYY-MM"
|
||||||
|
site: string; // code site ("292", "579")
|
||||||
|
stock_fin_mois: string; // stock fin de mois
|
||||||
|
prmp_fin_mois: string; // PRMP fin de mois
|
||||||
|
ventes: FfMensuelVentes | null;
|
||||||
|
receptions: FfMensuelReceptions | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données mensuelles (stock + ventes + réceptions) pour un lot d'articles.
|
||||||
|
* Utilise no_id comme identifiant (obligatoire pour l'API).
|
||||||
|
* Retourne Map<codein, FfMensuelEntry[]>
|
||||||
|
*/
|
||||||
|
export async function getMensuelByArticles(
|
||||||
|
articles: FfArticle[],
|
||||||
|
dateDebut: string,
|
||||||
|
dateFin: string,
|
||||||
|
batchSize = 20
|
||||||
|
): Promise<Map<string, FfMensuelEntry[]>> {
|
||||||
|
const result = new Map<string, FfMensuelEntry[]>();
|
||||||
|
|
||||||
|
for (let i = 0; i < articles.length; i += batchSize) {
|
||||||
|
const batch = articles.slice(i, i + batchSize);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
batch.map(async (art) => {
|
batch.map(async (art) => {
|
||||||
|
const noId = art.noid;
|
||||||
|
if (!noId) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const url = `${FF_API_BASE}/api/articles/${encodeURIComponent(String(noId))}/mensuel?dateDebut=${dateDebut}&dateFin=${dateFin}`;
|
||||||
`${FF_API_BASE}/api/stocks/articles?codein=${encodeURIComponent(art.codein)}`,
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
{ cache: "no-store" }
|
|
||||||
);
|
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const rawSites = extractList(data);
|
const entries: FfMensuelEntry[] = Array.isArray(data.data) ? data.data : [];
|
||||||
if (rawSites.length === 0) return;
|
if (entries.length > 0) {
|
||||||
|
result.set(art.codein, entries);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const sites: FfStock[] = rawSites.map((s: any) => ({
|
|
||||||
codein: s.codein ?? s.Codein ?? art.codein,
|
|
||||||
site: s.site ?? s.Site ?? s.codesite ?? s.magasin ?? "",
|
|
||||||
stockdispo: Number(s.stockdispo ?? s.StockDispo ?? s.stock_dispo ?? s.stock ?? 0),
|
|
||||||
qte: Number(s.qte ?? s.Qte ?? s.quantite ?? 0),
|
|
||||||
valstock: Number(s.valstock ?? s.ValStock ?? s.val_stock ?? s.valeur ?? 0),
|
|
||||||
prmp: Number(s.prmp ?? s.Prmp ?? s.pa ?? s.prix_achat ?? 0) || undefined,
|
|
||||||
pv: Number(s.pv ?? s.Pv ?? s.prix_vente ?? s.pv_central ?? 0) || undefined,
|
|
||||||
dernieresventes: s.dernieresventes ?? s.DernVentes ?? s.derniere_vente ?? s.last_sale ?? "",
|
|
||||||
dernierereception: s.dernierereception ?? s.DernReception ?? s.derniere_reception ?? s.last_reception ?? "",
|
|
||||||
nbjoursdernsventes: Number(s.nbjoursdernsventes ?? s.NbJoursDernVentes ?? s.nb_jours ?? 0) || undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const agg: AggregatedStock = {
|
|
||||||
stockActuel: 0,
|
|
||||||
stockTotal: 0,
|
|
||||||
stockValeur: 0,
|
|
||||||
pa: 0,
|
|
||||||
prixVente: 0,
|
|
||||||
derniereVente: "",
|
|
||||||
derniereLivraison: "",
|
|
||||||
nbJoursDerniereVente: 9999,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const s of sites) {
|
|
||||||
agg.stockActuel += s.stockdispo ?? 0;
|
|
||||||
agg.stockTotal += s.qte ?? 0;
|
|
||||||
agg.stockValeur += s.valstock ?? 0;
|
|
||||||
if (!agg.pa && s.prmp) agg.pa = s.prmp;
|
|
||||||
if (!agg.prixVente && s.pv) agg.prixVente = s.pv;
|
|
||||||
if (s.dernieresventes && s.dernieresventes > agg.derniereVente) {
|
|
||||||
agg.derniereVente = s.dernieresventes;
|
|
||||||
}
|
|
||||||
if (s.dernierereception && s.dernierereception > agg.derniereLivraison) {
|
|
||||||
agg.derniereLivraison = s.dernierereception;
|
|
||||||
}
|
|
||||||
if (s.nbjoursdernsventes !== undefined && s.nbjoursdernsventes < agg.nbJoursDerniereVente) {
|
|
||||||
agg.nbJoursDerniereVente = s.nbjoursdernsventes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (agg.nbJoursDerniereVente === 9999) agg.nbJoursDerniereVente = 0;
|
|
||||||
|
|
||||||
result.set(art.codein, agg);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[api-ff] getStock error for ${art.codein}:`, err);
|
console.error(`[api-ff] getMensuel error for ${art.codein}:`, err);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user