FIFO Lager Bewertung
+ +| Produkt | +Code | +Kategorie | +Stückkosten | +Menge | +Wert | +
|---|
diff --git a/AGENTS.md b/AGENTS.md index 2e6b9f5..e42924d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,9 +66,14 @@ No bundler — keep everything in global scope as the file already does. `query` selects the dataset. The datasets are large, so **exactly one dataset is requested per call**, for the dashboard currently shown (no prefetching). - Query keys: `"Summe je Abgangsland"` (Umsatz je Abgangsland), `"VAT Summary"`, - `"ODOO Rechnungen pro Monat"`, `"Alle Rechnungen pro Monat"` and `"Quartal Schweiz"`. - Each view's `query` lives in the `VIEWS` map, which maps the active dashboard to its - `normalize` function; `quarter: true` marks the dashboards that use a quarter period. + `"ODOO Rechnungen pro Monat"`, `"Alle Rechnungen pro Monat"`, `"Quartal Schweiz"`, + `"FIFO Lager Bewertung"` and `"Produkt Materialkosten"`. Each view's `query` lives in + the `VIEWS` map, which maps the active dashboard to its `normalize` function; + `quarter: true` marks the dashboards that use a quarter period. The FIFO and + Produkt-Materialkosten responses are `[{ id, query, row_count, data: [...] }]` + (unwrapped in `normalizeFifo` / `normalizeMaterialkosten`). In *Produkt Materialkosten*, + `remaining_qty` is the remaining quantity of that cost bracket (the last cost bracket, not FIFO), + not the product total. - `extractArray` accepts the dataset either as a bare array or wrapped under the query key (case-insensitive). If the shape changes upstream, adjust the normalizers. - The webhook returns **German-formatted strings**, e.g. `"90.717,93 €"` and diff --git a/README.md b/README.md index 2f717e9..240c988 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ The left sidebar groups reports by topic: - **Amazon** → *Umsatz je Abgangsland* (Brutto inkl. USt je Abgangsland) - **Amazon** → *VAT Summary* (USt-Übersicht je Land) - **ODOO** → *Rechnungen pro Monat* (Pivot über Odoo-Rechnungen) +- **ODOO** → *FIFO Lager Bewertung* (Lagerwert FIFO) +- **ODOO** → *Produkt Materialkosten* (Materialkosten je Produkt) - **Gesamt** → *Alle Rechnungen pro Monat* (PivotTable.js-Experiment) - **Gesamt** → *Quartal Schweiz* (AMAZON + ODOO, Lieferung in die Schweiz) @@ -60,6 +62,8 @@ Gesamt row: - *Rechnungen pro Monat* → `odoo-rechnungen_YYYY-MMM.csv` - *Alle Rechnungen pro Monat* → `gesamt-rechnungen-pivot_YYYY-MMM.csv` - *Quartal Schweiz* → `schweiz-quartal_YYYY-QN.csv` +- *FIFO Lager Bewertung* → `fifo-lager_YYYY-MMM.csv` +- *Produkt Materialkosten* → `materialkosten_YYYY-MMM.csv` The button is disabled while no dataset is loaded for the current dashboard. @@ -81,6 +85,7 @@ Rechnungen pro Monat* filters by **Tag**, **Bestellnummer**, **Lieferland** and For *Quartal Schweiz* the shared **Zeitraum** control shows a **quarter** selector (Q1–Q4) instead of the month selector; the year selector is shared. + ## Pivot (Experiment) The three invoice dashboards (*ODOO Rechnungen pro Monat*, *Alle Rechnungen pro @@ -118,6 +123,8 @@ selected with the `query` field: - *Rechnungen pro Monat* → `query: "ODOO Rechnungen pro Monat"` - *Alle Rechnungen pro Monat* → `query: "Alle Rechnungen pro Monat"` - *Quartal Schweiz* → `query: "Quartal Schweiz"` (sends `quarter` instead of `month`) +- *FIFO Lager Bewertung* → `query: "FIFO Lager Bewertung"` (response is `[{ id, query, row_count, data: [...] }]`) +- *Produkt Materialkosten* → `query: "Produkt Materialkosten"` (one row per product; `remaining_qty` = remaining of that cost bracket, not the product total) ## Files diff --git a/app.js b/app.js index 0b85490..c8a36c0 100644 --- a/app.js +++ b/app.js @@ -55,6 +55,15 @@ const els = { chFilterLieferland: document.getElementById("ch-filter-lieferland"), chFilterNummer: document.getElementById("ch-filter-rechnungsnummer"), chFilterQuelle: document.getElementById("ch-filter-quelle"), + // ODOO · FIFO Lager Bewertung + fifoKpis: document.getElementById("fifo-kpis"), + fifoTable: document.querySelector("#fifo-table tbody"), + fifoInfo: document.getElementById("fifo-info"), + fifoState: document.getElementById("fifo-state"), + // ODOO · Produkt Materialkosten + mkTable: document.querySelector("#mk-table tbody"), + mkInfo: document.getElementById("mk-info"), + mkState: document.getElementById("mk-state"), // Auth authOverlay: document.getElementById("auth-overlay"), authForm: document.getElementById("auth-form"), @@ -195,6 +204,10 @@ function formatQuantity(value) { return value.toLocaleString("de-DE"); } +function formatDate(value) { + return value ? String(value).slice(0, 10) : ""; +} + /* ---------- Data ---------- */ function extractArray(payload, key) { @@ -313,6 +326,89 @@ function normalizeRechnungen(rows) { return { invoices, totals: computeRechnungenTotals(invoices) }; } +function pickLocalized(value) { + if (value == null) return ""; + if (typeof value !== "object") return String(value); + return value.de_DE ?? value.de_CH ?? value.en_US ?? Object.values(value)[0] ?? ""; +} + +function normalizeFifo(rows) { + // The webhook returns [{ id, query, row_count, data: [...] }]. + let items = Array.isArray(rows) ? rows : []; + if (items.length && items[0] && Array.isArray(items[0].data)) { + items = items.flatMap((r) => (Array.isArray(r.data) ? r.data : [])); + } + + const products = items.map((row) => { + const unitCost = parseAmount(row.unit_cost); + const qty = parseAmount(row.remaining_qty); + return { + productId: row.product_id, + code: row.default_code ?? "", + name: pickLocalized(row.product_name), + categId: row.categ_id, + unitCost, + qty, + value: unitCost * qty, + }; + }); + + const totals = products.reduce( + (acc, p) => { + acc.value += p.value; + acc.qty += p.qty; + return acc; + }, + { value: 0, qty: 0 } + ); + + return { products, totals, productCount: new Set(products.map((p) => p.productId)).size }; +} + +function normalizeMaterialkosten(rows) { + // Response: [{ id, query, row_count, data: [...] }], one row per product. + // remaining_qty = remaining quantity of that cost bracket, NOT the total. + let items = Array.isArray(rows) ? rows : []; + if (items.length && items[0] && Array.isArray(items[0].data)) { + items = items.flatMap((r) => (Array.isArray(r.data) ? r.data : [])); + } + + const products = items.map((row) => { + const unitCost = parseAmount(row.unit_cost); + const qty = parseAmount(row.remaining_qty); + return { + productId: row.product_id, + code: row.default_code ?? "", + name: pickLocalized(row.product_name), + categId: row.categ_id, + unitCost, + qty, + value: unitCost * qty, + createDate: formatDate(row.create_date), + }; + }); + + // Sort by category, then product name. + products.sort((a, b) => { + const ca = Number(a.categId) || 0; + const cb = Number(b.categId) || 0; + if (ca !== cb) return ca - cb; + return String(a.name).localeCompare(String(b.name), "de"); + }); + + const totals = products.reduce( + (acc, p) => { + acc.value += p.value; + acc.qty += p.qty; + return acc; + }, + { value: 0, qty: 0 } + ); + + return { products, totals, productCount: new Set(products.map((p) => p.productId)).size }; +} + + /* ---------- ODOO Rechnungen pro Monat: filters ---------- */ let rechnungenFilter = { tag: "", bestellnummer: "", lieferland: "", nummer: "", quelle: "" }; @@ -646,6 +742,90 @@ function renderRechnungenKpis(data) { `; } +function renderFifo(data) { + const v = viewEls(); + if (!v) return; + const { products, totals, productCount } = data; + + v.kpis.innerHTML = ` +