Stückkosten
This commit is contained in:
1 parent
c816ea3ac9
commit
d7c55d444b
5 files changed
+321
-6
No files matched your search
@@ -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) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderFifo(data) {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
const { products, totals, productCount } = data;
|
||||
|
||||
v.kpis.innerHTML = `
|
||||
<div class="kpi primary">
|
||||
<div class="kpi-label">Lagerwert (FIFO)</div>
|
||||
<div class="kpi-value">${formatEuro(totals.value)}</div>
|
||||
<div class="kpi-sub">Summe Stückkosten × Restmenge</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Produkte</div>
|
||||
<div class="kpi-value">${productCount}</div>
|
||||
<div class="kpi-sub">verschiedene Artikel</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Menge</div>
|
||||
<div class="kpi-value">${formatQuantity(totals.qty)}</div>
|
||||
<div class="kpi-sub">Restmenge gesamt</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Positionen</div>
|
||||
<div class="kpi-value">${products.length}</div>
|
||||
<div class="kpi-sub">FIFO-Schichten</div>
|
||||
</div>`;
|
||||
|
||||
if (els.fifoInfo) els.fifoInfo.textContent = `${products.length} Positionen`;
|
||||
|
||||
v.table.innerHTML = "";
|
||||
products.forEach((p) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${p.name}</td>
|
||||
<td>${p.code}</td>
|
||||
<td class="num">${p.categId ?? ""}</td>
|
||||
<td class="num">${formatEuro(p.unitCost)}</td>
|
||||
<td class="num">${formatQuantity(p.qty)}</td>
|
||||
<td class="num">${formatEuro(p.value)}</td>`;
|
||||
v.table.appendChild(tr);
|
||||
});
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "total";
|
||||
tr.innerHTML = `
|
||||
<td>Gesamt</td>
|
||||
<td></td>
|
||||
<td class="num"></td>
|
||||
<td class="num"></td>
|
||||
<td class="num">${formatQuantity(totals.qty)}</td>
|
||||
<td class="num">${formatEuro(totals.value)}</td>`;
|
||||
v.table.appendChild(tr);
|
||||
}
|
||||
|
||||
function renderMaterialkosten(data) {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
const { products, totals } = data;
|
||||
|
||||
if (els.mkInfo) els.mkInfo.textContent = `${products.length} Positionen`;
|
||||
|
||||
v.table.innerHTML = "";
|
||||
products.forEach((p) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${p.name}</td>
|
||||
<td class="num">${p.categId ?? ""}</td>
|
||||
<td>${p.createDate}</td>
|
||||
<td class="num">${formatEuro(p.unitCost)}</td>
|
||||
<td class="num">${formatQuantity(p.qty)}</td>`;
|
||||
v.table.appendChild(tr);
|
||||
});
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "total";
|
||||
tr.innerHTML = `
|
||||
<td>Gesamt</td>
|
||||
<td class="num"></td>
|
||||
<td></td>
|
||||
<td class="num"></td>
|
||||
<td class="num">${formatQuantity(totals.qty)}</td>`;
|
||||
v.table.appendChild(tr);
|
||||
}
|
||||
|
||||
function renderRechnungenChart(data) {
|
||||
const v = viewEls();
|
||||
if (!v || !v.chart) return;
|
||||
@@ -792,6 +972,12 @@ function viewEls() {
|
||||
if (currentView === "odoo-rechnungen") {
|
||||
return { kpis: els.odooKpis, chart: els.odooChart, legend: els.odooLegend, state: els.odooState, pivot: document.getElementById("odoo-pivot") };
|
||||
}
|
||||
if (currentView === "fifo-lager") {
|
||||
return { kpis: els.fifoKpis, table: els.fifoTable, state: els.fifoState };
|
||||
}
|
||||
if (currentView === "materialkosten") {
|
||||
return { table: els.mkTable, state: els.mkState };
|
||||
}
|
||||
if (currentView === "gesamt-rechnungen-pivot") {
|
||||
return { kpis: els.pivotKpis, state: els.pivotState, pivot: document.getElementById("pivot") };
|
||||
}
|
||||
@@ -809,6 +995,10 @@ function renderActiveView() {
|
||||
renderVat(lastData.data);
|
||||
} else if (currentView === "odoo-rechnungen" || currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal") {
|
||||
renderRechnungenPivot(applyRechnungenFilters(lastData.data));
|
||||
} else if (currentView === "fifo-lager") {
|
||||
renderFifo(lastData.data);
|
||||
} else if (currentView === "materialkosten") {
|
||||
renderMaterialkosten(lastData.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,7 +1018,7 @@ function showSkeleton() {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
v.state.classList.add("hidden");
|
||||
v.kpis.innerHTML = skeletonKpis();
|
||||
if (v.kpis) v.kpis.innerHTML = skeletonKpis();
|
||||
if (v.chart) v.chart.innerHTML = skeletonBars();
|
||||
if (v.legend) v.legend.innerHTML = "";
|
||||
if (v.table) v.table.innerHTML = "";
|
||||
@@ -845,7 +1035,7 @@ function showError(message) {
|
||||
function clearViews() {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
v.kpis.innerHTML = "";
|
||||
if (v.kpis) v.kpis.innerHTML = "";
|
||||
if (v.chart) v.chart.innerHTML = "";
|
||||
if (v.legend) v.legend.innerHTML = "";
|
||||
if (v.table) v.table.innerHTML = "";
|
||||
@@ -1009,6 +1199,34 @@ function buildRechnungenCsv(data) {
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function buildFifoCsv(data) {
|
||||
const header = ["Produkt", "Code", "Kategorie", "Stückkosten", "Menge", "Wert"];
|
||||
const lines = [header.map(csvEscape).join(";")];
|
||||
data.products.forEach((p) => {
|
||||
lines.push([
|
||||
p.name, p.code, p.categId ?? "", formatEuro(p.unitCost), formatQuantity(p.qty), formatEuro(p.value),
|
||||
].map(csvEscape).join(";"));
|
||||
});
|
||||
lines.push([
|
||||
"Gesamt", "", "", "", formatQuantity(data.totals.qty), formatEuro(data.totals.value),
|
||||
].map(csvEscape).join(";"));
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function buildMaterialkostenCsv(data) {
|
||||
const header = ["Produkt", "Kategorie", "Einlagerungsdatum", "Stückkosten", "Restmenge (Staffel)"];
|
||||
const lines = [header.map(csvEscape).join(";")];
|
||||
data.products.forEach((p) => {
|
||||
lines.push([
|
||||
p.name, p.categId ?? "", p.createDate, formatEuro(p.unitCost), formatQuantity(p.qty),
|
||||
].map(csvEscape).join(";"));
|
||||
});
|
||||
lines.push([
|
||||
"Gesamt", "", "", "", formatQuantity(data.totals.qty),
|
||||
].map(csvEscape).join(";"));
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function downloadCsv() {
|
||||
if (!lastData || lastData.view !== currentView) return;
|
||||
|
||||
@@ -1022,6 +1240,12 @@ function downloadCsv() {
|
||||
} else if (currentView === "amazon-vat-summary") {
|
||||
csv = buildVatCsv(lastData.data);
|
||||
filename = `vat-summary_${period}.csv`;
|
||||
} else if (currentView === "materialkosten") {
|
||||
csv = buildMaterialkostenCsv(lastData.data);
|
||||
filename = `materialkosten_${period}.csv`;
|
||||
} else if (currentView === "fifo-lager") {
|
||||
csv = buildFifoCsv(lastData.data);
|
||||
filename = `fifo-lager_${period}.csv`;
|
||||
} else if (currentView === "odoo-rechnungen" || currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal") {
|
||||
csv = buildRechnungenCsv(applyRechnungenFilters(lastData.data));
|
||||
filename = `${currentView}_${period}.csv`;
|
||||
@@ -1048,7 +1272,9 @@ function updateDownloadState() {
|
||||
currentView === "amazon-vat-summary" ||
|
||||
currentView === "odoo-rechnungen" ||
|
||||
currentView === "gesamt-rechnungen-pivot" ||
|
||||
currentView === "schweiz-quartal";
|
||||
currentView === "schweiz-quartal" ||
|
||||
currentView === "fifo-lager" ||
|
||||
currentView === "materialkosten";
|
||||
els.download.disabled = !(lastData && lastData.view === currentView && supported);
|
||||
}
|
||||
|
||||
@@ -1074,6 +1300,18 @@ const VIEWS = {
|
||||
query: "ODOO Rechnungen pro Monat",
|
||||
normalize: normalizeRechnungen,
|
||||
},
|
||||
"fifo-lager": {
|
||||
title: "FIFO Lager Bewertung",
|
||||
subtitle: "ODOO · Lagerwert (FIFO)",
|
||||
query: "FIFO Lager Bewertung",
|
||||
normalize: normalizeFifo,
|
||||
},
|
||||
"materialkosten": {
|
||||
title: "Produkt Materialkosten",
|
||||
subtitle: "ODOO · Materialkosten je Produkt (Kostenstaffel)",
|
||||
query: "Produkt Materialkosten",
|
||||
normalize: normalizeMaterialkosten,
|
||||
},
|
||||
"gesamt-rechnungen-pivot": {
|
||||
title: "Alle Rechnungen pro Monat",
|
||||
subtitle: "Gesamt · Pivot",
|
||||
|
||||
Reference in new issue
Block a user