Compare commits
4
Commits
1ea3de74f2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8e52e0875 | ||
|
|
d7c55d444b | ||
|
|
c816ea3ac9 | ||
|
|
9db080faea |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{}
|
||||
@@ -0,0 +1,138 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for agents working in this repository (Brandize · BI Dashboard).
|
||||
|
||||
## What this project is
|
||||
|
||||
A **flat, server-free browser dashboard** for the Brandize BI webhook. It renders
|
||||
gross revenue (Brutto inkl. USt) per departure country plus a VAT summary, with KPI
|
||||
cards, bar charts and detail tables. There is **no build step, no framework, no
|
||||
package manager** — just static files opened directly in a browser.
|
||||
|
||||
Supporting tools: a bash dev-server launcher (`start`) and a Python CLI (`send`) that
|
||||
uploads CSV rows to an n8n webhook. SQL scripts (`sql/`) define the upstream queries
|
||||
that feed the webhook (Odoo 17 / PostgreSQL).
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `index.html` | Dashboard markup: sidebar nav, shared filter bar, one `<main class="view">` per report, auth overlay |
|
||||
| `styles.css` | All styling (CSS custom properties in `:root`, BEM-ish class names) |
|
||||
| `app.js` | Everything: data loading, parsing, rendering, CSV export, auth, navigation |
|
||||
| `README.md` | User-facing docs (German/English mixed). Update it when behavior changes |
|
||||
| `start` | Bash: installs `live-server` if missing and serves the folder on :8080 |
|
||||
| `send` | Python CLI: batches CSV rows to the `.../webhook/VAT` n8n webhook |
|
||||
| `sql/*.sql` | Upstream Odoo/PostgreSQL queries (see below) |
|
||||
| `data/` | Git-ignored. Empty in the repo; holds local CSV payloads for `send` |
|
||||
|
||||
## Running / validating
|
||||
|
||||
There is **no test suite and no linter**. Validation is manual:
|
||||
|
||||
- Open `index.html` directly (double-click) — the intended primary usage, works over `file://`.
|
||||
- Or run `./start` and browse `http://localhost:8080` for live reload.
|
||||
- `./send data/<file>.csv` requires Python 3 + `requests` and network access to the webhook.
|
||||
|
||||
Because `fetch` hits a remote webhook protected by HTTP Basic Auth, you generally
|
||||
**cannot fully exercise the UI without valid credentials and network access**. Don't
|
||||
claim validation passed unless you actually loaded the page and saw it work; otherwise
|
||||
state clearly what you could not run.
|
||||
|
||||
## Architecture notes (`app.js`)
|
||||
|
||||
Single file, loaded as a plain `<script>` (not a module), `"use strict"` at the top.
|
||||
No bundler — keep everything in global scope as the file already does.
|
||||
|
||||
- **Config constants** at the top: `API_URL`, `MONTHS` (German abbreviations), `PALETTE`.
|
||||
- **`els`** — a single object caching all `getElementById`/`querySelector` lookups.
|
||||
Add new DOM handles here rather than querying inline.
|
||||
- **State**: `currentView`, `lastData` (`{ abgangsland, vat }`), `auth`.
|
||||
- **Sections** are delimited by `/* ---------- Name ---------- */` banners: Auth,
|
||||
Period selector, Formatting, Data, Rendering (per report), CSV export, Navigation, Init.
|
||||
- **Views**: the `VIEWS` map defines title/subtitle per view id. `showView(name)`
|
||||
toggles `.hidden` on the `<main class="view">` elements and re-renders. A new report
|
||||
needs: markup section in `index.html`, an entry in `VIEWS`, a render function, and a
|
||||
`viewEls()` branch.
|
||||
- **Data flow**: `load()` → `POST API_URL` with `{ query, month }` → `extractArray(payload, key)`
|
||||
→ `normalizeAbgangsland` / `normalizeVat` → `renderActiveView()`. `load()` queries
|
||||
**only** the active dashboard's dataset; it is triggered from `showView()` (when a
|
||||
dashboard is displayed) and from the refresh/period controls — never prefetched.
|
||||
|
||||
### Webhook / data contract
|
||||
|
||||
- The webhook is called with **POST** and a JSON body `{ "query": <key>, "month": "YYYY-MMM" }`
|
||||
(quarter dashboards send `{ "query": <key>, "quarter": "YYYY-QN" }` instead).
|
||||
`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"`, `"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
|
||||
`"1.234"`. Parsing (`parseEuro`, `parseQuantity`) strips symbols and swaps
|
||||
`.`/`,` — do **not** use `parseFloat` directly on the raw values.
|
||||
- `month` uses `YYYY-MMM` (e.g. `2026-SEP`), matching `%(month)s` in `sql/*.sql`;
|
||||
`quarter` uses `YYYY-QN` (e.g. `2026-Q3`), matching `%(quarter)s` in `sql/Quartal_Schweiz.sql`.
|
||||
|
||||
### Auth
|
||||
|
||||
HTTP Basic. The first request goes out without credentials; a `401` triggers the
|
||||
login overlay. Credentials live in `sessionStorage` (best-effort — unavailable on
|
||||
`file://`, where they stay in memory only) and are sent as `Authorization: Basic ...`.
|
||||
|
||||
## SQL scripts (`sql/`)
|
||||
|
||||
Odoo 17 / PostgreSQL against table `brandize_datacenter_amazon_vat_transaction`.
|
||||
Each file has a header block documenting the model, parameters and invocation.
|
||||
|
||||
- Parameter is named `%(month)s` for psycopg2; in `psql` use `:'month'` instead
|
||||
(documented in the headers — respect this distinction).
|
||||
- Money is formatted **in SQL** to German notation via
|
||||
`translate(to_char(...), ',.', '.,') || ' €'` so the frontend receives display-ready
|
||||
strings. Keep formatting consistent if you add columns.
|
||||
- `Delete_Activity_Period.sql` is destructive — note the "ACHTUNG" warning and the
|
||||
transaction-bracket guidance. Be cautious editing it.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Language**: UI text is **German**, code identifiers and comments are English
|
||||
(comments occasionally German). Keep new user-facing strings German.
|
||||
- **Number/date formatting**: always `de-DE` via `toLocaleString` in JS and German
|
||||
`to_char` formatting in SQL. Dates as `YYYY-MMM` with the `MONTHS` abbreviations.
|
||||
- **JS style**: `"use strict"`, double quotes, semicolons, 2-space indent, `const`/`let`
|
||||
(no `var`), named `function` declarations, `camelCase`. Nullish-safe (`??`, optional
|
||||
guards). DOM built imperatively with `document.createElement` (charts are hand-rolled
|
||||
bars/lists, no chart library).
|
||||
- **CSS**: use the existing `:root` custom properties for colors/spacing/radius; reuse
|
||||
existing class patterns rather than inventing new naming.
|
||||
- **CSV export**: semicolon-delimited, UTF-8 **with BOM** (for Excel umlauts), CRLF line
|
||||
endings, includes a `Gesamt` total row, filename `*_YYYY-MMM.csv`. Follow this pattern
|
||||
for any new exportable view.
|
||||
- Keep changes minimal and consistent with the existing file structure — this is a small,
|
||||
intentionally dependency-free static app. Prefer no new dependencies.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `data/` is git-ignored; never commit contents from it. `send` expects CSVs with a
|
||||
header row (`csv.DictReader`) and posts JSON arrays in batches of 50.
|
||||
- The `send` script targets a **different** webhook (`/webhook/VAT`) than the dashboard
|
||||
(`/webhook/bi-dashboard`). Don't conflate them.
|
||||
- `fetch` from `file://` sends `Origin: null`; the webhook reflects origins to allow CORS.
|
||||
Changing how requests are made can break the "just open the file" workflow.
|
||||
- README documents behavior (navigation, auth, CSV, filter). If you change any of these,
|
||||
update `README.md` in the same change.
|
||||
- The invoice dashboards (*ODOO Rechnungen pro Monat*, *Alle Rechnungen pro Monat*
|
||||
and *Quartal Schweiz*) are pivot experiments: they load jQuery, jQuery UI and
|
||||
PivotTable.js from CDNs (so they need internet), reuse `normalizeRechnungen`, and
|
||||
render via `renderRechnungenPivot` (no detail table). Layouts come from
|
||||
`PIVOT_PRESETS` (keyed per view; the Vorlage dropdown) / the pivotUI panel; the
|
||||
renderer is fixed to *Table*. The pivot container is recreated on each render so
|
||||
a preset actually applies.
|
||||
@@ -15,6 +15,12 @@ The page calls the webhook directly:
|
||||
https://brandize.app.n8n.cloud/webhook/bi-dashboard
|
||||
```
|
||||
|
||||
via a **POST** request with a JSON body:
|
||||
|
||||
```json
|
||||
{ "query": "Summe je Abgangsland", "month": "2026-SEP" }
|
||||
```
|
||||
|
||||
The webhook sends CORS headers (it reflects the request `Origin`, including
|
||||
`null` for `file://`), so a direct browser call works.
|
||||
|
||||
@@ -24,9 +30,17 @@ The left sidebar groups reports by topic:
|
||||
|
||||
- **Amazon** → *Umsatz je Abgangsland* (Brutto inkl. USt je Abgangsland)
|
||||
- **Amazon** → *VAT Summary* (USt-Übersicht je Land)
|
||||
- **ODOO** → *Dashboard* (placeholder, to follow)
|
||||
- **Amazon** → *Cross Boarder Inv. Movement* (In Bearbeitung)
|
||||
- **Amazon** → *Auswertung Buchhaltung* (In Bearbeitung)
|
||||
- **Amazon** → *ESL/ZM-Check* (In Bearbeitung)
|
||||
- **Amazon** → *Inventory Summary* (In Bearbeitung)
|
||||
- **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)
|
||||
|
||||
Both groups start collapsed, so no submenu is visible when the page opens.
|
||||
All groups start collapsed, so no submenu is visible when the page opens.
|
||||
Clicking a topic expands its submenu; clicking a submenu item switches the
|
||||
view.
|
||||
|
||||
@@ -49,8 +63,13 @@ Gesamt row:
|
||||
|
||||
- *Umsatz je Abgangsland* → `umsatz-je-abgangsland_YYYY-MMM.csv`
|
||||
- *VAT Summary* → `vat-summary_YYYY-MMM.csv`
|
||||
- *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 on dashboards without data (e.g. the ODOO placeholder).
|
||||
The button is disabled while no dataset is loaded for the current dashboard.
|
||||
|
||||
## Filter
|
||||
|
||||
@@ -58,15 +77,58 @@ A shared filter bar sits above every dashboard (labelled **Filter** with a
|
||||
funnel icon and the hint *gilt für alle Dashboards*). It holds the **Zeitraum**
|
||||
month + year selector (e.g. `2026-SEP`) and the reload button, and stays visible
|
||||
across all views. The previous month is preselected. The selected period is
|
||||
sent to the webhook as `?month=YYYY-MMM`, matching the `%(month)s` parameter
|
||||
used in `../sql/Summe_je_Abgangsland.sql`.
|
||||
sent to the webhook in the POST body as `month` (e.g. `2026-SEP`), matching the
|
||||
`%(month)s` parameter used in `../sql/*.sql`.
|
||||
|
||||
The invoice dashboards (*ODOO Rechnungen pro Monat*, *Alle Rechnungen pro Monat*
|
||||
and *Quartal Schweiz*) have an additional, dashboard-specific filter row. *ODOO
|
||||
Rechnungen pro Monat* filters by **Tag**, **Bestellnummer**, **Lieferland** and
|
||||
**Rechnungsnummer**; *Alle Rechnungen pro Monat* and *Quartal Schweiz* add a
|
||||
**Quelle** field. Filtering re-renders the KPIs, chart and pivot live.
|
||||
|
||||
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
|
||||
Monat* and *Quartal Schweiz*) render their data as an interactive
|
||||
[PivotTable.js](https://pivottable.js.org/) pivot instead of a detail table.
|
||||
The pivot's rows, columns, values and aggregator are freely configurable in the
|
||||
pivotUI control panel; the renderer is fixed to *Table*. Each dashboard has its
|
||||
own filter row and **Vorlage** dropdown with presets:
|
||||
|
||||
- *ODOO Rechnungen pro Monat*: *Standard* (**Rechnungsland** rows, no column
|
||||
group, **Sum(Gesamtpreis brutto)**) and *Zahlart*.
|
||||
- *Alle Rechnungen pro Monat* and *Quartal Schweiz*: *Standard* (**Quelle**
|
||||
rows, no column group, **Sum(Gesamtpreis brutto)**) and *Rechnungsland
|
||||
(Spalten)* (adds **Rechnungsland** as the column group).
|
||||
|
||||
*Quartal Schweiz* is filtered to invoices with a **delivery into Switzerland**.
|
||||
|
||||
Applying a preset also resets the filters. The chosen layout is kept while you
|
||||
type in the filters.
|
||||
|
||||
It loads jQuery, jQuery UI and PivotTable.js from public CDNs, so — unlike the
|
||||
rest of the app — it needs an **internet connection** to render; offline it shows
|
||||
a short message instead. To vendor the libraries locally, download them next to
|
||||
`index.html` and change the `<link>`/`<script>` URLs in the `<head>`.
|
||||
|
||||
## Data sources
|
||||
|
||||
One request to the webhook returns both datasets, which are read by key:
|
||||
Because the datasets are large, the dashboard requests **only the dataset of
|
||||
the currently displayed dashboard** — one POST per dashboard, sent only when
|
||||
that dashboard is opened or reloaded. Nothing is prefetched. The dataset is
|
||||
selected with the `query` field:
|
||||
|
||||
- `Summe je Abgangsland` → the *Umsatz je Abgangsland* report
|
||||
- `VAT Summary` → the *VAT Summary* report
|
||||
- *Umsatz je Abgangsland* → `query: "Summe je Abgangsland"`
|
||||
- *VAT Summary* → `query: "VAT Summary"`
|
||||
- *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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const PALETTE = ["#4f46e5", "#0ea5e9", "#f59e0b", "#10b981", "#ec4899", "#8b5cf6
|
||||
|
||||
const els = {
|
||||
month: document.getElementById("month-select"),
|
||||
quarter: document.getElementById("quarter-select"),
|
||||
year: document.getElementById("year-select"),
|
||||
refresh: document.getElementById("refresh-btn"),
|
||||
download: document.getElementById("download-btn"),
|
||||
@@ -23,6 +24,46 @@ const els = {
|
||||
vatTable: document.querySelector("#vat-table tbody"),
|
||||
vatPeriod: document.getElementById("vat-period"),
|
||||
vatState: document.getElementById("vat-state"),
|
||||
// ODOO · Rechnungen pro Monat
|
||||
odooKpis: document.getElementById("odoo-kpis"),
|
||||
odooChart: document.getElementById("odoo-chart"),
|
||||
odooLegend: document.getElementById("odoo-legend"),
|
||||
odooPeriod: document.getElementById("odoo-period"),
|
||||
odooState: document.getElementById("odoo-state"),
|
||||
odooPivotPreset: document.getElementById("odoo-pivot-preset"),
|
||||
odooFilterTag: document.getElementById("odoo-filter-tag"),
|
||||
odooFilterBestellnummer: document.getElementById("odoo-filter-bestellnummer"),
|
||||
odooFilterLieferland: document.getElementById("odoo-filter-lieferland"),
|
||||
odooFilterNummer: document.getElementById("odoo-filter-rechnungsnummer"),
|
||||
// Gesamt · Alle Rechnungen pro Monat (Pivot, Experiment)
|
||||
pivotKpis: document.getElementById("pivot-kpis"),
|
||||
pivotPeriod: document.getElementById("pivot-period"),
|
||||
pivotState: document.getElementById("pivot-state"),
|
||||
pivotPreset: document.getElementById("pivot-preset"),
|
||||
pivotFilterTag: document.getElementById("pivot-filter-tag"),
|
||||
pivotFilterBestellnummer: document.getElementById("pivot-filter-bestellnummer"),
|
||||
pivotFilterLieferland: document.getElementById("pivot-filter-lieferland"),
|
||||
pivotFilterNummer: document.getElementById("pivot-filter-rechnungsnummer"),
|
||||
pivotFilterQuelle: document.getElementById("pivot-filter-quelle"),
|
||||
// Schweiz · Quartal Schweiz
|
||||
chKpis: document.getElementById("ch-kpis"),
|
||||
chPeriod: document.getElementById("ch-period"),
|
||||
chState: document.getElementById("ch-state"),
|
||||
chPreset: document.getElementById("ch-pivot-preset"),
|
||||
chFilterTag: document.getElementById("ch-filter-tag"),
|
||||
chFilterBestellnummer: document.getElementById("ch-filter-bestellnummer"),
|
||||
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"),
|
||||
@@ -33,8 +74,10 @@ const els = {
|
||||
};
|
||||
|
||||
// State for the currently displayed dashboard and its last loaded data.
|
||||
// Only the dataset of the active dashboard is held — nothing is prefetched.
|
||||
let currentView = "amazon-abgangsland";
|
||||
let lastData = null; // { abgangsland, vat }
|
||||
let lastData = null; // { view, data } — data is the normalized dataset of `view`
|
||||
let loadToken = 0; // guards against stale responses from superseded requests
|
||||
|
||||
/* ---------- Auth (HTTP Basic) ---------- */
|
||||
|
||||
@@ -121,13 +164,17 @@ function buildPeriodSelectors() {
|
||||
}
|
||||
|
||||
els.month.value = String(defMonth);
|
||||
els.quarter.value = String(Math.floor(defMonth / 3) + 1);
|
||||
els.year.value = String(defYear);
|
||||
}
|
||||
|
||||
function selectedPeriod() {
|
||||
const meta = VIEWS[currentView];
|
||||
if (meta && meta.quarter) {
|
||||
return `${els.year.value}-Q${els.quarter.value}`;
|
||||
}
|
||||
const month = MONTHS[Number(els.month.value)];
|
||||
const year = els.year.value;
|
||||
return `${year}-${month}`;
|
||||
return `${els.year.value}-${month}`;
|
||||
}
|
||||
|
||||
/* ---------- Formatting ---------- */
|
||||
@@ -157,9 +204,16 @@ function formatQuantity(value) {
|
||||
return value.toLocaleString("de-DE");
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
return value ? String(value).slice(0, 10) : "";
|
||||
}
|
||||
|
||||
/* ---------- Data ---------- */
|
||||
|
||||
function extractArray(payload, key) {
|
||||
// The webhook may return the requested dataset either directly as an array
|
||||
// or wrapped under its query key.
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
if (Array.isArray(payload[key])) return payload[key];
|
||||
const match = Object.keys(payload).find((k) => k.toLowerCase() === key.toLowerCase());
|
||||
@@ -217,6 +271,235 @@ function normalizeVat(rows) {
|
||||
return { countries, totals };
|
||||
}
|
||||
|
||||
// Amounts may arrive either as plain numbers (SQL numeric) or as German
|
||||
// formatted strings ("90.717,93 €") — accept both.
|
||||
function parseAmount(value) {
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
||||
return parseEuro(value);
|
||||
}
|
||||
|
||||
function computeRechnungenTotals(invoices) {
|
||||
return invoices.reduce(
|
||||
(acc, i) => {
|
||||
acc.gesamtBrutto += i.gesamtBrutto;
|
||||
acc.gesamtNetto += i.gesamtNetto;
|
||||
acc.brutto += i.brutto;
|
||||
acc.bruttoErm += i.bruttoErm;
|
||||
acc.netto += i.netto;
|
||||
acc.nettoErm += i.nettoErm;
|
||||
acc.versandBrutto += i.versandBrutto;
|
||||
acc.versandNetto += i.versandNetto;
|
||||
return acc;
|
||||
},
|
||||
{ gesamtBrutto: 0, gesamtNetto: 0, brutto: 0, bruttoErm: 0, netto: 0, nettoErm: 0, versandBrutto: 0, versandNetto: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRechnungen(rows) {
|
||||
const invoices = rows.map((row) => ({
|
||||
datum: row["Rechnungsdatum"] ?? "",
|
||||
nummer: row["Rechnungsnummer"] ?? "",
|
||||
land: row["Rechnungsland"] ?? "",
|
||||
landCode: row["Rechnungsland Ländercode"] ?? "",
|
||||
zahlart: row["Zahlart"] ?? "",
|
||||
zahlungsreferenz: row["Zahlungsreference der Bestellung"] ?? "",
|
||||
vatId: row["USt. Id / VAT Id des Käufers (in Bestellung)"] ?? "",
|
||||
gesamtBrutto: parseAmount(row["Gesamtpreis brutto"]),
|
||||
gesamtNetto: parseAmount(row["Gesamtpreis netto"]),
|
||||
brutto: parseAmount(row["Brutto"]),
|
||||
bruttoErm: parseAmount(row["Brutto erm"]),
|
||||
ustSatz: parseAmount(row["USt. Satz"]),
|
||||
ustSatzErm: parseAmount(row["Ust. Satz erm"]),
|
||||
netto: parseAmount(row["Netto Betrag"]),
|
||||
nettoErm: parseAmount(row["Netto Betrag erm"]),
|
||||
versandBrutto: parseAmount(row["Versandkosten brutto"]),
|
||||
versandNetto: parseAmount(row["Versandkosten netto"]),
|
||||
auftragsdatum: row["Auftragsdatum"] ?? "",
|
||||
bestellnummer: row["Bestellnummer"] ?? "",
|
||||
zahldatum: row["Zahldatum"] ?? "",
|
||||
lieferName: row["Lieferadresse Name"] ?? "",
|
||||
tag: row["Tag"] ?? "",
|
||||
lieferland: row["Lieferland"] ?? "",
|
||||
quelle: row["Quelle"] ?? "",
|
||||
}));
|
||||
|
||||
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: "" };
|
||||
|
||||
function matchesFilter(value, query) {
|
||||
if (!query) return true;
|
||||
return String(value).toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
function applyRechnungenFilters(data) {
|
||||
const f = rechnungenFilter;
|
||||
const invoices = data.invoices.filter((i) =>
|
||||
matchesFilter(i.tag, f.tag) &&
|
||||
matchesFilter(i.bestellnummer, f.bestellnummer) &&
|
||||
matchesFilter(i.lieferland, f.lieferland) &&
|
||||
matchesFilter(i.nummer, f.nummer) &&
|
||||
matchesFilter(i.quelle, f.quelle)
|
||||
);
|
||||
return { invoices, totals: computeRechnungenTotals(invoices) };
|
||||
}
|
||||
|
||||
function rechnungenFilterInputs() {
|
||||
if (currentView === "gesamt-rechnungen-pivot") {
|
||||
return [els.pivotFilterTag, els.pivotFilterBestellnummer, els.pivotFilterLieferland, els.pivotFilterNummer, els.pivotFilterQuelle];
|
||||
}
|
||||
if (currentView === "schweiz-quartal") {
|
||||
return [els.chFilterTag, els.chFilterBestellnummer, els.chFilterLieferland, els.chFilterNummer, els.chFilterQuelle];
|
||||
}
|
||||
if (currentView === "odoo-rechnungen") {
|
||||
return [els.odooFilterTag, els.odooFilterBestellnummer, els.odooFilterLieferland, els.odooFilterNummer];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Every filter input across the invoice dashboards. Attached once at startup,
|
||||
// because the views are only toggled — the inputs stay in the DOM.
|
||||
function allRechnungenFilterInputs() {
|
||||
return [
|
||||
els.odooFilterTag, els.odooFilterBestellnummer, els.odooFilterLieferland, els.odooFilterNummer,
|
||||
els.pivotFilterTag, els.pivotFilterBestellnummer, els.pivotFilterLieferland, els.pivotFilterNummer, els.pivotFilterQuelle,
|
||||
els.chFilterTag, els.chFilterBestellnummer, els.chFilterLieferland, els.chFilterNummer, els.chFilterQuelle,
|
||||
];
|
||||
}
|
||||
|
||||
function resetRechnungenFilters() {
|
||||
rechnungenFilter = { tag: "", bestellnummer: "", lieferland: "", nummer: "", quelle: "" };
|
||||
rechnungenFilterInputs().forEach((input) => { if (input) input.value = ""; });
|
||||
}
|
||||
|
||||
function onRechnungenFilter() {
|
||||
const [tag, bestellnummer, lieferland, nummer, quelle] = rechnungenFilterInputs();
|
||||
rechnungenFilter = {
|
||||
tag: (tag?.value || "").trim(),
|
||||
bestellnummer: (bestellnummer?.value || "").trim(),
|
||||
lieferland: (lieferland?.value || "").trim(),
|
||||
nummer: (nummer?.value || "").trim(),
|
||||
quelle: (quelle?.value || "").trim(),
|
||||
};
|
||||
if (lastData && lastData.view === currentView) {
|
||||
renderActiveView();
|
||||
}
|
||||
}
|
||||
|
||||
function setupRechnungenFilters() {
|
||||
allRechnungenFilterInputs().forEach((input) => { if (input) input.addEventListener("input", onRechnungenFilter); });
|
||||
}
|
||||
|
||||
function setupRechnungenPivot() {
|
||||
wirePivotPreset(els.odooPivotPreset, "odoo-rechnungen");
|
||||
wirePivotPreset(els.pivotPreset, "gesamt-rechnungen-pivot");
|
||||
wirePivotPreset(els.chPreset, "schweiz-quartal");
|
||||
}
|
||||
|
||||
function wirePivotPreset(select, view) {
|
||||
if (!select) return;
|
||||
select.addEventListener("change", () => {
|
||||
const presets = PIVOT_PRESETS[view];
|
||||
const preset = presets[select.value] || presets.standard;
|
||||
pivotOptions[view] = {
|
||||
rows: [...preset.rows],
|
||||
cols: [...preset.cols],
|
||||
vals: [...preset.vals],
|
||||
aggregatorName: preset.aggregatorName,
|
||||
rendererName: preset.rendererName,
|
||||
};
|
||||
// A preset also loads the default (empty) filters.
|
||||
resetRechnungenFilters();
|
||||
renderActiveView();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Rendering: Umsatz je Abgangsland ---------- */
|
||||
|
||||
function colorFor(bereich, map) {
|
||||
@@ -428,6 +711,255 @@ function renderVat(data) {
|
||||
renderVatTable(data);
|
||||
}
|
||||
|
||||
/* ---------- Rendering: ODOO Rechnungen pro Monat ---------- */
|
||||
|
||||
function renderRechnungenKpis(data) {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
const { invoices, totals } = data;
|
||||
const ust = totals.gesamtBrutto - totals.gesamtNetto;
|
||||
|
||||
v.kpis.innerHTML = `
|
||||
<div class="kpi primary">
|
||||
<div class="kpi-label">Gesamt brutto</div>
|
||||
<div class="kpi-value">${formatEuro(totals.gesamtBrutto)}</div>
|
||||
<div class="kpi-sub">alle Rechnungen im Zeitraum</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Rechnungen</div>
|
||||
<div class="kpi-value">${invoices.length}</div>
|
||||
<div class="kpi-sub">Datensätze</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">Gesamt netto</div>
|
||||
<div class="kpi-value">${formatEuro(totals.gesamtNetto)}</div>
|
||||
<div class="kpi-sub">ohne USt</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">USt (brutto − netto)</div>
|
||||
<div class="kpi-value">${formatEuro(ust)}</div>
|
||||
<div class="kpi-sub">enthaltene Umsatzsteuer</div>
|
||||
</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;
|
||||
|
||||
const byCountry = new Map();
|
||||
data.invoices.forEach((inv) => {
|
||||
const key = inv.landCode || inv.land || "–";
|
||||
byCountry.set(key, (byCountry.get(key) || 0) + inv.gesamtBrutto);
|
||||
});
|
||||
|
||||
const entries = [...byCountry.entries()].sort((a, b) => b[1] - a[1]);
|
||||
const grand = entries.reduce((sum, [, value]) => sum + value, 0);
|
||||
const max = entries.reduce((m, [, value]) => Math.max(m, value), 0) || 1;
|
||||
|
||||
v.chart.innerHTML = "";
|
||||
v.legend.innerHTML = "";
|
||||
|
||||
if (!entries.length) {
|
||||
v.chart.innerHTML = '<p class="muted">Keine Daten für diesen Zeitraum.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach(([code, value]) => {
|
||||
const pct = (value / max) * 100;
|
||||
const share = grand ? (value / grand) * 100 : 0;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "bar-row";
|
||||
row.innerHTML = `
|
||||
<div class="bar-country">${code}</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill" style="background:var(--accent)"></div>
|
||||
</div>
|
||||
<div class="bar-value">${formatEuro(value)} · ${formatPercent(share)}</div>`;
|
||||
v.chart.appendChild(row);
|
||||
|
||||
const fill = row.querySelector(".bar-fill");
|
||||
fill.getBoundingClientRect();
|
||||
fill.style.width = Math.max(pct, 6) + "%";
|
||||
fill.classList.add("ready");
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Rechnungen pivot (experiment) ---------- */
|
||||
|
||||
// Preset pivot layouts per view, selectable from each view's "Vorlage" dropdown.
|
||||
const PIVOT_PRESETS = {
|
||||
"gesamt-rechnungen-pivot": {
|
||||
standard: { rows: ["Quelle"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
rechnungsland: { rows: ["Quelle"], cols: ["Rechnungsland"], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
"odoo-rechnungen": {
|
||||
standard: { rows: ["Rechnungsland"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
zahlart: { rows: ["Zahlart"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
"schweiz-quartal": {
|
||||
standard: { rows: ["Quelle"], cols: [], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
rechnungsland: { rows: ["Quelle"], cols: ["Rechnungsland"], vals: ["Gesamtpreis brutto"], aggregatorName: "Sum", rendererName: "Table" },
|
||||
},
|
||||
};
|
||||
|
||||
// Current pivot layout per view (kept so a filter re-render doesn't reset it).
|
||||
const pivotOptions = {
|
||||
"gesamt-rechnungen-pivot": { ...PIVOT_PRESETS["gesamt-rechnungen-pivot"].standard },
|
||||
"odoo-rechnungen": { ...PIVOT_PRESETS["odoo-rechnungen"].standard },
|
||||
"schweiz-quartal": { ...PIVOT_PRESETS["schweiz-quartal"].standard },
|
||||
};
|
||||
|
||||
function renderRechnungenPivot(data) {
|
||||
const v = viewEls();
|
||||
if (!v || !v.pivot) return;
|
||||
|
||||
renderRechnungenKpis(data);
|
||||
if (v.chart) renderRechnungenChart(data);
|
||||
|
||||
// Recreate the container so pivotUI cannot reuse state from a previous
|
||||
// render — clearing innerHTML alone is not enough, so a preset change would
|
||||
// not take effect.
|
||||
const prev = v.pivot;
|
||||
const el = prev.cloneNode(false);
|
||||
prev.parentNode.replaceChild(el, prev);
|
||||
|
||||
if (!window.jQuery || !jQuery.fn.pivotUI) {
|
||||
el.innerHTML = '<p class="muted">PivotTable.js ist nicht geladen (offline?).</p>';
|
||||
return;
|
||||
}
|
||||
if (!data.invoices.length) {
|
||||
el.innerHTML = '<p class="muted">Keine Daten für diesen Zeitraum.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Flaches Datenmodell für die Pivot-Tabelle. Alle Attribute sind im
|
||||
// pivotUI-Bedienfeld frei kombinierbar (Zeilen/Spalten/Werte/Aggregator).
|
||||
// Hinweis: "Brutto" ist nur der Normalsatz-Anteil; "Gesamtpreis brutto" ist
|
||||
// Brutto + Brutto erm (= die Summe, die die KPI "Gesamt brutto" anzeigt).
|
||||
const rows = data.invoices.map((i) => ({
|
||||
Quelle: i.quelle || "(leer)",
|
||||
Rechnungsland: i.land || "(leer)",
|
||||
Zahlart: i.zahlart || "(leer)",
|
||||
Bestellnummer: i.bestellnummer || "(leer)",
|
||||
Tag: i.tag || "(leer)",
|
||||
Lieferland: i.lieferland || "(leer)",
|
||||
"Gesamtpreis brutto": i.gesamtBrutto,
|
||||
Brutto: i.brutto,
|
||||
"Brutto erm": i.bruttoErm,
|
||||
"Gesamtpreis netto": i.gesamtNetto,
|
||||
Netto: i.netto,
|
||||
"Netto erm": i.nettoErm,
|
||||
"Versandkosten brutto": i.versandBrutto,
|
||||
"Versandkosten netto": i.versandNetto,
|
||||
}));
|
||||
|
||||
const opts = {
|
||||
...pivotOptions[currentView],
|
||||
unusedAttrsVertical: true,
|
||||
onRefresh: (config) => {
|
||||
pivotOptions[currentView] = {
|
||||
rows: config.rows,
|
||||
cols: config.cols,
|
||||
vals: config.vals,
|
||||
aggregatorName: config.aggregatorName,
|
||||
rendererName: config.rendererName,
|
||||
};
|
||||
},
|
||||
};
|
||||
// Only offer the plain table renderer (no heatmaps/charts).
|
||||
const tableRenderer = jQuery.pivotUtilities && jQuery.pivotUtilities.renderers
|
||||
? jQuery.pivotUtilities.renderers["Table"]
|
||||
: null;
|
||||
if (tableRenderer) opts.renderers = { Table: tableRenderer };
|
||||
|
||||
jQuery(el).pivotUI(rows, opts);
|
||||
}
|
||||
|
||||
/* ---------- Active view helpers ---------- */
|
||||
|
||||
function viewEls() {
|
||||
@@ -437,15 +969,36 @@ function viewEls() {
|
||||
if (currentView === "amazon-vat-summary") {
|
||||
return { kpis: els.vatKpis, chart: els.vatChart, legend: els.vatLegend, table: els.vatTable, state: els.vatState };
|
||||
}
|
||||
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") };
|
||||
}
|
||||
if (currentView === "schweiz-quartal") {
|
||||
return { kpis: els.chKpis, state: els.chState, pivot: document.getElementById("ch-pivot") };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderActiveView() {
|
||||
if (!lastData) return;
|
||||
if (!lastData || lastData.view !== currentView) return;
|
||||
if (currentView === "amazon-abgangsland") {
|
||||
renderAbgangsland(lastData.abgangsland);
|
||||
renderAbgangsland(lastData.data);
|
||||
} else if (currentView === "amazon-vat-summary") {
|
||||
renderVat(lastData.vat);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,10 +1018,11 @@ function showSkeleton() {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
v.state.classList.add("hidden");
|
||||
v.kpis.innerHTML = skeletonKpis();
|
||||
v.chart.innerHTML = skeletonBars();
|
||||
v.legend.innerHTML = "";
|
||||
v.table.innerHTML = "";
|
||||
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 = "";
|
||||
if (v.pivot) v.pivot.innerHTML = "";
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
@@ -481,25 +1035,53 @@ function showError(message) {
|
||||
function clearViews() {
|
||||
const v = viewEls();
|
||||
if (!v) return;
|
||||
v.kpis.innerHTML = "";
|
||||
v.chart.innerHTML = "";
|
||||
v.legend.innerHTML = "";
|
||||
v.table.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 = "";
|
||||
if (v.pivot) v.pivot.innerHTML = "";
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const meta = VIEWS[currentView];
|
||||
const query = meta && meta.query;
|
||||
|
||||
// The datasets are huge: only the dataset of the displayed dashboard is
|
||||
// requested, and only when that dashboard is shown or reloaded.
|
||||
if (!query) {
|
||||
lastData = null;
|
||||
clearViews();
|
||||
updateDownloadState();
|
||||
return;
|
||||
}
|
||||
|
||||
resetRechnungenFilters();
|
||||
|
||||
const period = selectedPeriod();
|
||||
els.tablePeriod.textContent = period;
|
||||
els.vatPeriod.textContent = period;
|
||||
els.odooPeriod.textContent = period;
|
||||
els.pivotPeriod.textContent = period;
|
||||
els.chPeriod.textContent = period;
|
||||
els.refresh.classList.add("spinning");
|
||||
showSkeleton();
|
||||
|
||||
const token = ++loadToken;
|
||||
|
||||
try {
|
||||
const headers = { Accept: "application/json" };
|
||||
const headers = { Accept: "application/json", "Content-Type": "application/json" };
|
||||
const authorization = authHeader();
|
||||
if (authorization) headers.Authorization = authorization;
|
||||
|
||||
const response = await fetch(`${API_URL}?month=${encodeURIComponent(period)}`, { headers });
|
||||
const body = meta.quarter ? { query, quarter: period } : { query, month: period };
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// A newer request (view/period switch) superseded this one — ignore it.
|
||||
if (token !== loadToken) return;
|
||||
|
||||
// The webhook is protected with Basic Auth: ask for credentials when needed.
|
||||
if (response.status === 401) {
|
||||
@@ -515,10 +1097,7 @@ async function load() {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const payload = await response.json();
|
||||
lastData = {
|
||||
abgangsland: normalizeAbgangsland(extractArray(payload, "Summe je Abgangsland")),
|
||||
vat: normalizeVat(extractArray(payload, "VAT Summary")),
|
||||
};
|
||||
lastData = { view: currentView, data: meta.normalize(extractArray(payload, query)) };
|
||||
|
||||
renderActiveView();
|
||||
|
||||
@@ -528,6 +1107,7 @@ async function load() {
|
||||
v.state.textContent = "";
|
||||
}
|
||||
} catch (error) {
|
||||
if (token !== loadToken) return;
|
||||
lastData = null;
|
||||
clearViews();
|
||||
showError(`Daten konnten nicht geladen werden: ${error.message}`);
|
||||
@@ -578,19 +1158,97 @@ function buildVatCsv(data) {
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
function buildRechnungenCsv(data) {
|
||||
const showQuelle = currentView === "gesamt-rechnungen-pivot" || currentView === "schweiz-quartal";
|
||||
const header = [
|
||||
"Rechnungsdatum", "Rechnungsnummer", "Rechnungsland", "Rechnungsland Ländercode",
|
||||
"Zahlart", "Zahlungsreference der Bestellung",
|
||||
"USt. Id / VAT Id des Käufers (in Bestellung)", "Gesamtpreis brutto", "Gesamtpreis netto",
|
||||
"Brutto", "Brutto erm", "USt. Satz", "Ust. Satz erm", "Netto Betrag", "Netto Betrag erm",
|
||||
"Versandkosten brutto", "Versandkosten netto", "Auftragsdatum", "Bestellnummer", "Zahldatum",
|
||||
"Lieferadresse Name", "Tag", "Lieferland",
|
||||
];
|
||||
if (showQuelle) header.push("Quelle");
|
||||
const lines = [header.map(csvEscape).join(";")];
|
||||
|
||||
data.invoices.forEach((i) => {
|
||||
const cols = [
|
||||
i.datum, i.nummer, i.land, i.landCode, i.zahlart, i.zahlungsreferenz, i.vatId,
|
||||
formatEuro(i.gesamtBrutto), formatEuro(i.gesamtNetto),
|
||||
formatEuro(i.brutto), formatEuro(i.bruttoErm), formatQuantity(i.ustSatz), formatQuantity(i.ustSatzErm),
|
||||
formatEuro(i.netto), formatEuro(i.nettoErm),
|
||||
formatEuro(i.versandBrutto), formatEuro(i.versandNetto),
|
||||
i.auftragsdatum, i.bestellnummer, i.zahldatum, i.lieferName, i.tag, i.lieferland,
|
||||
];
|
||||
if (showQuelle) cols.push(i.quelle);
|
||||
lines.push(cols.map(csvEscape).join(";"));
|
||||
});
|
||||
|
||||
const t = data.totals;
|
||||
const totalCols = [
|
||||
"Gesamt", String(data.invoices.length), "", "", "", "", "",
|
||||
formatEuro(t.gesamtBrutto), formatEuro(t.gesamtNetto),
|
||||
formatEuro(t.brutto), formatEuro(t.bruttoErm), "", "",
|
||||
formatEuro(t.netto), formatEuro(t.nettoErm),
|
||||
formatEuro(t.versandBrutto), formatEuro(t.versandNetto),
|
||||
"", "", "", "", "", "",
|
||||
];
|
||||
if (showQuelle) totalCols.push("");
|
||||
lines.push(totalCols.map(csvEscape).join(";"));
|
||||
|
||||
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) return;
|
||||
if (!lastData || lastData.view !== currentView) return;
|
||||
|
||||
const period = selectedPeriod();
|
||||
let csv;
|
||||
let filename;
|
||||
|
||||
if (currentView === "amazon-abgangsland") {
|
||||
csv = buildAbgangslandCsv(lastData.abgangsland);
|
||||
csv = buildAbgangslandCsv(lastData.data);
|
||||
filename = `umsatz-je-abgangsland_${period}.csv`;
|
||||
} else if (currentView === "amazon-vat-summary") {
|
||||
csv = buildVatCsv(lastData.vat);
|
||||
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`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -609,8 +1267,15 @@ function downloadCsv() {
|
||||
}
|
||||
|
||||
function updateDownloadState() {
|
||||
const supported = currentView === "amazon-abgangsland" || currentView === "amazon-vat-summary";
|
||||
els.download.disabled = !(lastData && supported);
|
||||
const supported =
|
||||
currentView === "amazon-abgangsland" ||
|
||||
currentView === "amazon-vat-summary" ||
|
||||
currentView === "odoo-rechnungen" ||
|
||||
currentView === "gesamt-rechnungen-pivot" ||
|
||||
currentView === "schweiz-quartal" ||
|
||||
currentView === "fifo-lager" ||
|
||||
currentView === "materialkosten";
|
||||
els.download.disabled = !(lastData && lastData.view === currentView && supported);
|
||||
}
|
||||
|
||||
/* ---------- Navigation ---------- */
|
||||
@@ -619,14 +1284,62 @@ const VIEWS = {
|
||||
"amazon-abgangsland": {
|
||||
title: "Umsatz je Abgangsland",
|
||||
subtitle: "Amazon · Brutto (inkl. USt)",
|
||||
// Webhook query key and row normalizer for this dashboard's dataset.
|
||||
query: "Summe je Abgangsland",
|
||||
normalize: normalizeAbgangsland,
|
||||
},
|
||||
"amazon-vat-summary": {
|
||||
title: "VAT Summary",
|
||||
subtitle: "Amazon · USt-Übersicht je Land",
|
||||
query: "VAT Summary",
|
||||
normalize: normalizeVat,
|
||||
},
|
||||
"odoo-dashboard": {
|
||||
title: "ODOO Dashboard",
|
||||
subtitle: "ODOO",
|
||||
"amazon-cross-border": {
|
||||
title: "Cross Boarder Inv. Movement",
|
||||
subtitle: "Amazon · In Bearbeitung",
|
||||
},
|
||||
"amazon-buchhaltung": {
|
||||
title: "Auswertung Buchhaltung",
|
||||
subtitle: "Amazon · In Bearbeitung",
|
||||
},
|
||||
"amazon-esl-zm": {
|
||||
title: "ESL/ZM-Check",
|
||||
subtitle: "Amazon · In Bearbeitung",
|
||||
},
|
||||
"amazon-inventory-summary": {
|
||||
title: "Inventory Summary",
|
||||
subtitle: "Amazon · In Bearbeitung",
|
||||
},
|
||||
"odoo-rechnungen": {
|
||||
title: "ODOO Rechnungen pro Monat",
|
||||
subtitle: "ODOO · Rechnungen je Monat",
|
||||
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",
|
||||
query: "Alle Rechnungen pro Monat",
|
||||
normalize: normalizeRechnungen,
|
||||
},
|
||||
"schweiz-quartal": {
|
||||
title: "Quartal Schweiz",
|
||||
subtitle: "Schweiz · AMAZON + ODOO",
|
||||
query: "Quartal Schweiz",
|
||||
normalize: normalizeRechnungen,
|
||||
quarter: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -664,7 +1377,12 @@ function showView(name) {
|
||||
document.getElementById("view-subtitle").textContent = meta.subtitle;
|
||||
|
||||
currentView = name;
|
||||
renderActiveView();
|
||||
// Quarter views use the quarter selector instead of the month selector.
|
||||
const isQuarter = !!meta.quarter;
|
||||
els.month.classList.toggle("hidden", isQuarter);
|
||||
els.quarter.classList.toggle("hidden", !isQuarter);
|
||||
// Query only this dashboard's dataset — nothing is prefetched.
|
||||
load();
|
||||
updateDownloadState();
|
||||
}
|
||||
|
||||
@@ -692,7 +1410,11 @@ function setupAuth() {
|
||||
buildPeriodSelectors();
|
||||
setupMenu();
|
||||
setupAuth();
|
||||
setupRechnungenFilters();
|
||||
setupRechnungenPivot();
|
||||
load();
|
||||
els.month.addEventListener("change", load);
|
||||
els.quarter.addEventListener("change", load);
|
||||
els.year.addEventListener("change", load);
|
||||
els.refresh.addEventListener("click", load);
|
||||
els.download.addEventListener("click", downloadCsv);
|
||||
|
||||
+287
-13
@@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Brandize · BI Dashboard</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<!-- PivotTable.js experiment (needs jQuery UI styles) -->
|
||||
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pivottable@2.23.0/dist/pivot.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
@@ -45,6 +48,18 @@
|
||||
<a class="submenu-item" href="#" data-view="amazon-vat-summary">
|
||||
VAT Summary
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="amazon-cross-border">
|
||||
Cross Boarder Inv. Movement (In Bearbeitung)
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="amazon-buchhaltung">
|
||||
Auswertung Buchhaltung (In Bearbeitung)
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="amazon-esl-zm">
|
||||
ESL/ZM-Check (In Bearbeitung)
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="amazon-inventory-summary">
|
||||
Inventory Summary (In Bearbeitung)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,8 +80,40 @@
|
||||
</svg>
|
||||
</button>
|
||||
<div class="submenu">
|
||||
<a class="submenu-item" href="#" data-view="odoo-dashboard">
|
||||
Dashboard
|
||||
<a class="submenu-item" href="#" data-view="odoo-rechnungen">
|
||||
ODOO Rechnungen pro Monat
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="fifo-lager">
|
||||
FIFO Lager Bewertung
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="materialkosten">
|
||||
Produkt Materialkosten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gesamt -->
|
||||
<div class="menu-group" data-group>
|
||||
<button class="menu-main" type="button" data-toggle aria-expanded="false">
|
||||
<span class="menu-logo">
|
||||
<svg viewBox="0 0 48 48" aria-hidden="true">
|
||||
<rect width="48" height="48" rx="12" fill="#4f46e5" />
|
||||
<text x="24" y="32" text-anchor="middle" font-size="24" font-weight="700"
|
||||
fill="#ffffff" font-family="Arial, sans-serif">Σ</text>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="menu-name">Gesamt</span>
|
||||
<svg class="chevron" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="submenu">
|
||||
<a class="submenu-item" href="#" data-view="gesamt-rechnungen-pivot">
|
||||
Alle Rechnungen pro Monat
|
||||
</a>
|
||||
<a class="submenu-item" href="#" data-view="schweiz-quartal">
|
||||
Quartal Schweiz
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,6 +155,12 @@
|
||||
<span class="field-label">Zeitraum</span>
|
||||
<div class="period">
|
||||
<select id="month-select" aria-label="Monat"></select>
|
||||
<select id="quarter-select" aria-label="Quartal" class="hidden">
|
||||
<option value="1">Q1</option>
|
||||
<option value="2">Q2</option>
|
||||
<option value="3">Q3</option>
|
||||
<option value="4">Q4</option>
|
||||
</select>
|
||||
<select id="year-select" aria-label="Jahr"></select>
|
||||
</div>
|
||||
</label>
|
||||
@@ -192,20 +245,237 @@
|
||||
<div class="state" id="vat-state"></div>
|
||||
</main>
|
||||
|
||||
<!-- View: ODOO · Dashboard (placeholder) -->
|
||||
<main id="view-odoo-dashboard" class="view hidden">
|
||||
<!-- View: Amazon · Cross Boarder Inv. Movement (In Bearbeitung) -->
|
||||
<main id="view-amazon-cross-border" class="view hidden">
|
||||
<section class="panel placeholder">
|
||||
<div class="placeholder-icon">
|
||||
<svg viewBox="0 0 48 48" aria-hidden="true">
|
||||
<rect width="48" height="48" rx="12" fill="#714B67" />
|
||||
<text x="24" y="30" text-anchor="middle" font-size="14" font-weight="700"
|
||||
fill="#ffffff" font-family="Arial, sans-serif">odoo</text>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>ODOO Dashboard</h2>
|
||||
<p class="muted">Dieses Dashboard folgt in Kürze.</p>
|
||||
<h2>Cross Boarder Inv. Movement</h2>
|
||||
<p class="muted">In Bearbeitung – dieses Dashboard folgt in Kürze.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- View: Amazon · Auswertung Buchhaltung (In Bearbeitung) -->
|
||||
<main id="view-amazon-buchhaltung" class="view hidden">
|
||||
<section class="panel placeholder">
|
||||
<h2>Auswertung Buchhaltung</h2>
|
||||
<p class="muted">In Bearbeitung – dieses Dashboard folgt in Kürze.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- View: Amazon · ESL/ZM-Check (In Bearbeitung) -->
|
||||
<main id="view-amazon-esl-zm" class="view hidden">
|
||||
<section class="panel placeholder">
|
||||
<h2>ESL/ZM-Check</h2>
|
||||
<p class="muted">In Bearbeitung – dieses Dashboard folgt in Kürze.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- View: Amazon · Inventory Summary (In Bearbeitung) -->
|
||||
<main id="view-amazon-inventory-summary" class="view hidden">
|
||||
<section class="panel placeholder">
|
||||
<h2>Inventory Summary</h2>
|
||||
<p class="muted">In Bearbeitung – dieses Dashboard folgt in Kürze.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- View: ODOO · Rechnungen pro Monat -->
|
||||
<main id="view-odoo-rechnungen" class="view hidden">
|
||||
<section class="kpis" id="odoo-kpis"></section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Gesamtpreis brutto je Rechnungsland</h2>
|
||||
<div class="legend" id="odoo-legend"></div>
|
||||
</div>
|
||||
<div class="chart" id="odoo-chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Pivot — Summe Gesamtpreis brutto</h2>
|
||||
<div class="panel-head-actions">
|
||||
<span class="muted" id="odoo-period"></span>
|
||||
<label class="field">
|
||||
<span class="field-label">Vorlage</span>
|
||||
<select id="odoo-pivot-preset" aria-label="Pivot-Vorlage">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="zahlart">Zahlart</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-filters">
|
||||
<label class="field">
|
||||
<span class="field-label">Tag</span>
|
||||
<input id="odoo-filter-tag" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Bestellnummer</span>
|
||||
<input id="odoo-filter-bestellnummer" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Lieferland</span>
|
||||
<input id="odoo-filter-lieferland" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Rechnungsnummer</span>
|
||||
<input id="odoo-filter-rechnungsnummer" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="odoo-pivot"></div>
|
||||
</section>
|
||||
|
||||
<div class="state" id="odoo-state"></div>
|
||||
</main>
|
||||
|
||||
<!-- View: ODOO · FIFO Lager Bewertung -->
|
||||
<main id="view-fifo-lager" class="view hidden">
|
||||
<section class="kpis" id="fifo-kpis"></section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>FIFO Lager Bewertung</h2>
|
||||
<span class="muted" id="fifo-info"></span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="fifo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produkt</th>
|
||||
<th>Code</th>
|
||||
<th class="num">Kategorie</th>
|
||||
<th class="num">Stückkosten</th>
|
||||
<th class="num">Menge</th>
|
||||
<th class="num">Wert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="state" id="fifo-state"></div>
|
||||
</main>
|
||||
|
||||
<!-- View: ODOO · Produkt Materialkosten -->
|
||||
<main id="view-materialkosten" class="view hidden">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Produkt Materialkosten</h2>
|
||||
<span class="muted" id="mk-info"></span>
|
||||
</div>
|
||||
<p class="muted panel-note" id="mk-note">
|
||||
Restmenge = verbleibende Menge der jeweiligen Kostenstaffel – nicht der Gesamtbestand des Produkts. Letzte Kostenstaffel, nicht FIFO.
|
||||
</p>
|
||||
<div class="table-wrap">
|
||||
<table id="mk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produkt</th>
|
||||
<th class="num">Kategorie</th>
|
||||
<th>Einlagerungsdatum</th>
|
||||
<th class="num">Stückkosten</th>
|
||||
<th class="num" title="Verbleibende Menge der jeweiligen Kostenstaffel – nicht der Gesamtbestand des Produkts. Letzte Kostenstaffel, nicht FIFO.">Restmenge (Staffel)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="state" id="mk-state"></div>
|
||||
</main>
|
||||
|
||||
<!-- View: Gesamt · Alle Rechnungen pro Monat -->
|
||||
<main id="view-gesamt-rechnungen-pivot" class="view hidden">
|
||||
<section class="kpis" id="pivot-kpis"></section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Pivot — Summe Gesamtpreis brutto</h2>
|
||||
<div class="panel-head-actions">
|
||||
<span class="muted" id="pivot-period"></span>
|
||||
<label class="field">
|
||||
<span class="field-label">Vorlage</span>
|
||||
<select id="pivot-preset" aria-label="Pivot-Vorlage">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="rechnungsland">Rechnungsland (Spalten)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-filters">
|
||||
<label class="field">
|
||||
<span class="field-label">Quelle</span>
|
||||
<input id="pivot-filter-quelle" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Tag</span>
|
||||
<input id="pivot-filter-tag" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Bestellnummer</span>
|
||||
<input id="pivot-filter-bestellnummer" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Lieferland</span>
|
||||
<input id="pivot-filter-lieferland" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Rechnungsnummer</span>
|
||||
<input id="pivot-filter-rechnungsnummer" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="pivot"></div>
|
||||
</section>
|
||||
|
||||
<div class="state" id="pivot-state"></div>
|
||||
</main>
|
||||
|
||||
<!-- View: Schweiz · Quartal Schweiz (AMAZON + ODOO, Lieferung in die CH) -->
|
||||
<main id="view-schweiz-quartal" class="view hidden">
|
||||
<section class="kpis" id="ch-kpis"></section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Pivot — Quartal Schweiz (Summe Gesamtpreis brutto)</h2>
|
||||
<div class="panel-head-actions">
|
||||
<span class="muted" id="ch-period"></span>
|
||||
<label class="field">
|
||||
<span class="field-label">Vorlage</span>
|
||||
<select id="ch-pivot-preset" aria-label="Pivot-Vorlage">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="rechnungsland">Rechnungsland (Spalten)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-filters">
|
||||
<label class="field">
|
||||
<span class="field-label">Quelle</span>
|
||||
<input id="ch-filter-quelle" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Tag</span>
|
||||
<input id="ch-filter-tag" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Bestellnummer</span>
|
||||
<input id="ch-filter-bestellnummer" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Lieferland</span>
|
||||
<input id="ch-filter-lieferland" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Rechnungsnummer</span>
|
||||
<input id="ch-filter-rechnungsnummer" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="ch-pivot"></div>
|
||||
</section>
|
||||
|
||||
<div class="state" id="ch-state"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,6 +511,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- PivotTable.js experiment — requires jQuery + jQuery UI -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pivottable@2.23.0/dist/pivot.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,415 @@
|
||||
-- =============================================================================
|
||||
-- Alle Rechnungen pro Monat – Gesamt (ODOO + AMAZON)
|
||||
-- Odoo 17 / PostgreSQL
|
||||
--
|
||||
-- Zweck:
|
||||
-- Liefert dieselben Spalten wie Rechnungsexport.sql (ODOO-Kundenrechnungen),
|
||||
-- aber zusammengefuehrt (UNION ALL) mit den Datensaetzen aus der Amazon-
|
||||
-- Tabelle brandize_datacenter_amazon_vat_transaction. Zusaetzlich gibt die
|
||||
-- Spalte "Quelle" an, aus welchem System die Zeile stammt ('ODOO'/'AMAZON').
|
||||
--
|
||||
-- Modelle:
|
||||
-- ODOO : account.move (Kundenrechnungen / Gutschriften)
|
||||
-- AMAZON : brandize_datacenter_amazon_vat_transaction
|
||||
--
|
||||
-- Parameter:
|
||||
-- %(month)s Abrechnungszeitraum als Text, z. B. '2026-SEP'
|
||||
-- ODOO : Rechnungsdatum im Kalendermonat (siehe params-CTE)
|
||||
-- AMAZON: activity_period = %(month)s
|
||||
--
|
||||
-- Aufruf (Odoo / psycopg2):
|
||||
-- cr.execute(open('Alle_Rechnungen.sql').read(), {'month': '2026-SEP'})
|
||||
--
|
||||
-- In psql stattdessen:
|
||||
-- \set month '2026-SEP'
|
||||
--
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Spalten-Mapping ODOO -> AMAZON
|
||||
-- (Felder ohne Amazon-Pendant bleiben leer)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Rechnungsdatum <- transaction_complete_date (Fallback: depart)
|
||||
-- Rechnungsnummer <- vat_inv_number
|
||||
-- Rechnungsland <- Name zu sale_arrival_country (res_country)
|
||||
-- Rechnungsland Code <- sale_arrival_country
|
||||
-- Zahlart <- '' (kein Amazon-Feld)
|
||||
-- Zahlungsreference <- transaction_event_id
|
||||
-- USt. Id Kaeufer <- buyer_vat_number
|
||||
-- Gesamtpreis brutto <- total_activity_value_amt_vat_incl
|
||||
-- Gesamtpreis netto <- total_activity_value_amt_vat_excl
|
||||
-- Brutto / -erm <- Steuersplit nach effektivem Satz (Schwelle
|
||||
-- USt. Satz / -erm CH >= 8, sonst >= 16 - wie im ODOO-Export)
|
||||
-- Netto Betrag / -erm Satz = total_activity_value_vat_amt / netto * 100
|
||||
-- Versandkosten brutto <- total_ship_charge_amt_vat_incl
|
||||
-- Versandkosten netto <- total_ship_charge_amt_vat_excl
|
||||
-- Auftragsdatum <- transaction_depart_date
|
||||
-- Bestellnummer <- activity_transaction_id
|
||||
-- Zahldatum <- '' (kein Amazon-Feld)
|
||||
-- Lieferadresse Name <- buyer_name
|
||||
-- Tag <- '' (kein Amazon-Feld)
|
||||
-- Lieferland <- Name zu arrival_country (res_country)
|
||||
--
|
||||
-- ACHTUNG: Das Amazon-Mapping ist eine 1:1-Uebertragung auf die ODOO-Spalten.
|
||||
-- Die Datumszuordnung (Rechnungsdatum/Auftragsdatum) sowie Bestellnummer
|
||||
-- sollten gegen die tatsaechliche Amazon-Report-Semantik geprueft und hier
|
||||
-- ggf. angepasst werden.
|
||||
-- =============================================================================
|
||||
|
||||
WITH params AS (
|
||||
-- Kalendermonat aus %(month)s (z. B. '2026-SEP') ableiten (ODDO-Seite).
|
||||
SELECT
|
||||
to_date(
|
||||
split_part(%(month)s, '-', 1) || '-' ||
|
||||
CASE upper(split_part(%(month)s, '-', 2))
|
||||
WHEN 'JAN' THEN '01' WHEN 'FEB' THEN '02'
|
||||
WHEN 'MÄR' THEN '03' WHEN 'MAR' THEN '03' WHEN 'APR' THEN '04'
|
||||
WHEN 'MAI' THEN '05' WHEN 'MAY' THEN '05' WHEN 'JUN' THEN '06'
|
||||
WHEN 'JUL' THEN '07' WHEN 'AUG' THEN '08' WHEN 'SEP' THEN '09'
|
||||
WHEN 'OKT' THEN '10' WHEN 'OCT' THEN '10' WHEN 'NOV' THEN '11'
|
||||
WHEN 'DEZ' THEN '12' WHEN 'DEC' THEN '12'
|
||||
END,
|
||||
'YYYY-MM'
|
||||
) AS first_day
|
||||
),
|
||||
|
||||
-- ===========================================================================
|
||||
-- ODOO-Seite (identisch zu Rechnungsexport.sql)
|
||||
-- ===========================================================================
|
||||
|
||||
invoices AS (
|
||||
SELECT
|
||||
am.id,
|
||||
am.name,
|
||||
am.ref,
|
||||
am.move_type,
|
||||
am.invoice_date,
|
||||
am.invoice_origin,
|
||||
am.partner_id,
|
||||
am.partner_shipping_id,
|
||||
am.invoice_payment_term_id,
|
||||
am.payment_reference
|
||||
FROM account_move am
|
||||
CROSS JOIN params p
|
||||
WHERE am.invoice_date >= p.first_day
|
||||
AND am.invoice_date < p.first_day + INTERVAL '1 month'
|
||||
AND am.move_type IN ('out_invoice', 'out_refund')
|
||||
),
|
||||
|
||||
line_taxes AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
at.amount AS tax_rate,
|
||||
SUM(aml.price_subtotal) AS netto,
|
||||
SUM(aml.price_total) AS brutto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_move_line_account_tax_rel amlatr
|
||||
ON amlatr.account_move_line_id = aml.id
|
||||
JOIN account_tax at
|
||||
ON at.id = amlatr.account_tax_id
|
||||
GROUP BY aml.move_id, at.amount
|
||||
),
|
||||
|
||||
tax_split AS (
|
||||
SELECT
|
||||
lt.invoice_id,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_erm
|
||||
|
||||
FROM line_taxes lt
|
||||
JOIN invoices inv
|
||||
ON inv.id = lt.invoice_id
|
||||
JOIN res_partner rp
|
||||
ON rp.id = inv.partner_id
|
||||
LEFT JOIN res_country rc
|
||||
ON rc.id = rp.country_id
|
||||
GROUP BY lt.invoice_id
|
||||
),
|
||||
|
||||
shipping AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
ROUND(SUM(aml.price_total)::numeric, 2) AS shipping_brutto,
|
||||
ROUND(SUM(aml.price_subtotal)::numeric, 2) AS shipping_netto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN product_product pp
|
||||
ON pp.id = aml.product_id
|
||||
JOIN product_template pt
|
||||
ON pt.id = pp.product_tmpl_id
|
||||
WHERE pt.categ_id = 9
|
||||
GROUP BY aml.move_id
|
||||
),
|
||||
|
||||
invoice_tags AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
STRING_AGG(
|
||||
DISTINCT COALESCE(ct.name->>'en_US', ct.name->>'de_DE', ''),
|
||||
', ' ORDER BY COALESCE(ct.name->>'en_US', ct.name->>'de_DE', '')
|
||||
) AS tags
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
JOIN sale_order_tag_rel sotr
|
||||
ON sotr.order_id = so.id
|
||||
JOIN crm_tag ct
|
||||
ON ct.id = sotr.tag_id
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
invoice_transaction_reference AS (
|
||||
SELECT
|
||||
rel.invoice_id,
|
||||
pt.provider_reference,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY rel.invoice_id
|
||||
ORDER BY pt.id
|
||||
) AS rn
|
||||
FROM account_invoice_transaction_rel rel
|
||||
JOIN payment_transaction pt
|
||||
ON pt.id = rel.transaction_id
|
||||
),
|
||||
|
||||
sale_info AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
MIN(so.date_order) AS order_date
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
invoice_payments AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
MAX(apr.max_date) AS payment_date
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_account aa
|
||||
ON aa.id = aml.account_id
|
||||
AND aa.account_type = 'asset_receivable'
|
||||
JOIN account_partial_reconcile apr
|
||||
ON apr.debit_move_id = aml.id
|
||||
OR apr.credit_move_id = aml.id
|
||||
GROUP BY aml.move_id
|
||||
),
|
||||
|
||||
odoo_rows AS (
|
||||
SELECT
|
||||
TO_CHAR(inv.invoice_date, 'YYYY-MM-DD') AS "Rechnungsdatum",
|
||||
|
||||
CASE
|
||||
WHEN inv.move_type = 'out_refund'
|
||||
THEN COALESCE(inv.name, inv.ref, '') || '-gs'
|
||||
ELSE COALESCE(inv.name, inv.ref, '')
|
||||
END AS "Rechnungsnummer",
|
||||
|
||||
COALESCE(rc_inv.name->>'en_US', rc_inv.name->>'de_DE', '') AS "Rechnungsland",
|
||||
COALESCE(rc_inv.code, '') AS "Rechnungsland Ländercode",
|
||||
COALESCE(apt.name->>'en_US', apt.name->>'de_DE', '') AS "Zahlart",
|
||||
|
||||
COALESCE(itr.provider_reference, '') AS "Zahlungsreference der Bestellung",
|
||||
COALESCE(partner.vat, '') AS "USt. Id / VAT Id des Käufers (in Bestellung)",
|
||||
|
||||
ROUND(COALESCE(ts.brutto_normal, 0) + COALESCE(ts.brutto_erm, 0), 2) AS "Gesamtpreis brutto",
|
||||
ROUND(COALESCE(ts.netto_normal, 0) + COALESCE(ts.netto_erm, 0), 2) AS "Gesamtpreis netto",
|
||||
|
||||
COALESCE(ts.brutto_normal, 0) AS "Brutto",
|
||||
COALESCE(ts.brutto_erm, 0) AS "Brutto erm",
|
||||
COALESCE(ts.ust_satz_normal, 0) AS "USt. Satz",
|
||||
COALESCE(ts.ust_satz_erm, 0) AS "Ust. Satz erm",
|
||||
COALESCE(ts.netto_normal, 0) AS "Netto Betrag",
|
||||
COALESCE(ts.netto_erm, 0) AS "Netto Betrag erm",
|
||||
|
||||
COALESCE(sh.shipping_brutto, 0) AS "Versandkosten brutto",
|
||||
COALESCE(sh.shipping_netto, 0) AS "Versandkosten netto",
|
||||
|
||||
COALESCE(TO_CHAR(si.order_date, 'YYYY-MM-DD'), '') AS "Auftragsdatum",
|
||||
COALESCE(inv.invoice_origin, '') AS "Bestellnummer",
|
||||
COALESCE(TO_CHAR(ip.payment_date, 'YYYY-MM-DD'), '') AS "Zahldatum",
|
||||
|
||||
COALESCE(ship.name, partner.name, '') AS "Lieferadresse Name",
|
||||
COALESCE(it.tags, '') AS "Tag",
|
||||
COALESCE(
|
||||
rc_ship.name->>'en_US', rc_ship.name->>'de_DE',
|
||||
rc_inv.name->>'en_US', rc_inv.name->>'de_DE',
|
||||
''
|
||||
) AS "Lieferland"
|
||||
|
||||
FROM invoices inv
|
||||
JOIN res_partner partner
|
||||
ON partner.id = inv.partner_id
|
||||
LEFT JOIN res_country rc_inv
|
||||
ON rc_inv.id = partner.country_id
|
||||
LEFT JOIN res_partner ship
|
||||
ON ship.id = inv.partner_shipping_id
|
||||
LEFT JOIN res_country rc_ship
|
||||
ON rc_ship.id = ship.country_id
|
||||
LEFT JOIN account_payment_term apt
|
||||
ON apt.id = inv.invoice_payment_term_id
|
||||
LEFT JOIN tax_split ts
|
||||
ON ts.invoice_id = inv.id
|
||||
LEFT JOIN shipping sh
|
||||
ON sh.invoice_id = inv.id
|
||||
LEFT JOIN invoice_tags it
|
||||
ON it.invoice_id = inv.id
|
||||
LEFT JOIN invoice_transaction_reference itr
|
||||
ON itr.invoice_id = inv.id
|
||||
AND itr.rn = 1
|
||||
LEFT JOIN sale_info si
|
||||
ON si.invoice_id = inv.id
|
||||
LEFT JOIN invoice_payments ip
|
||||
ON ip.invoice_id = inv.id
|
||||
),
|
||||
|
||||
-- ===========================================================================
|
||||
-- AMAZON-Seite
|
||||
-- ===========================================================================
|
||||
|
||||
-- Basiszahlen + effektiver USt-Satz je Transaktionszeile.
|
||||
amazon_base AS (
|
||||
SELECT
|
||||
a.transaction_complete_date,
|
||||
a.transaction_depart_date,
|
||||
a.vat_inv_number,
|
||||
a.sale_arrival_country,
|
||||
a.arrival_country,
|
||||
a.transaction_event_id,
|
||||
a.buyer_vat_number,
|
||||
a.buyer_name,
|
||||
a.activity_transaction_id,
|
||||
COALESCE(a.total_activity_value_amt_vat_incl, 0) AS gross,
|
||||
COALESCE(a.total_activity_value_amt_vat_excl, 0) AS net,
|
||||
COALESCE(a.total_ship_charge_amt_vat_incl, 0) AS ship_gross,
|
||||
COALESCE(a.total_ship_charge_amt_vat_excl, 0) AS ship_net,
|
||||
CASE
|
||||
WHEN COALESCE(a.total_activity_value_amt_vat_excl, 0) <> 0
|
||||
THEN ROUND((COALESCE(a.total_activity_value_vat_amt, 0)
|
||||
/ a.total_activity_value_amt_vat_excl * 100)::numeric, 2)
|
||||
ELSE 0
|
||||
END AS rate
|
||||
FROM brandize_datacenter_amazon_vat_transaction a
|
||||
WHERE a.activity_period = %(month)s
|
||||
-- Optional: Gebuehrenzeilen ausschliessen (wie in VAT_Summary.sql):
|
||||
-- AND a.sales_channel <> 'AMAZON_FEE'
|
||||
),
|
||||
|
||||
amazon_rows AS (
|
||||
SELECT
|
||||
TO_CHAR(COALESCE(b.transaction_complete_date, b.transaction_depart_date), 'YYYY-MM-DD')
|
||||
AS "Rechnungsdatum",
|
||||
COALESCE(b.vat_inv_number, '') AS "Rechnungsnummer",
|
||||
COALESCE(rc_inv.name->>'en_US', rc_inv.name->>'de_DE', '') AS "Rechnungsland",
|
||||
COALESCE(b.sale_arrival_country, '') AS "Rechnungsland Ländercode",
|
||||
'' AS "Zahlart",
|
||||
COALESCE(b.transaction_event_id, '') AS "Zahlungsreference der Bestellung",
|
||||
COALESCE(b.buyer_vat_number, '') AS "USt. Id / VAT Id des Käufers (in Bestellung)",
|
||||
|
||||
ROUND(b.gross::numeric, 2) AS "Gesamtpreis brutto",
|
||||
ROUND(b.net::numeric, 2) AS "Gesamtpreis netto",
|
||||
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN ROUND(b.gross::numeric, 2) ELSE 0
|
||||
END AS "Brutto",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN ROUND(b.gross::numeric, 2) ELSE 0
|
||||
END AS "Brutto erm",
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN b.rate ELSE 0
|
||||
END AS "USt. Satz",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN b.rate ELSE 0
|
||||
END AS "Ust. Satz erm",
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN ROUND(b.net::numeric, 2) ELSE 0
|
||||
END AS "Netto Betrag",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN ROUND(b.net::numeric, 2) ELSE 0
|
||||
END AS "Netto Betrag erm",
|
||||
|
||||
ROUND(b.ship_gross::numeric, 2) AS "Versandkosten brutto",
|
||||
ROUND(b.ship_net::numeric, 2) AS "Versandkosten netto",
|
||||
|
||||
COALESCE(TO_CHAR(b.transaction_depart_date, 'YYYY-MM-DD'), '') AS "Auftragsdatum",
|
||||
COALESCE(b.activity_transaction_id, '') AS "Bestellnummer",
|
||||
'' AS "Zahldatum",
|
||||
COALESCE(b.buyer_name, '') AS "Lieferadresse Name",
|
||||
'' AS "Tag",
|
||||
COALESCE(rc_ship.name->>'en_US', rc_ship.name->>'de_DE', '') AS "Lieferland"
|
||||
|
||||
FROM amazon_base b
|
||||
LEFT JOIN res_country rc_inv
|
||||
ON rc_inv.code = b.sale_arrival_country
|
||||
LEFT JOIN res_country rc_ship
|
||||
ON rc_ship.code = b.arrival_country
|
||||
)
|
||||
|
||||
-- ===========================================================================
|
||||
-- Zusammenfuehrung: ODOO + AMAZON, je Zeile mit Quelle
|
||||
-- ===========================================================================
|
||||
SELECT *, 'ODOO' AS "Quelle" FROM odoo_rows
|
||||
UNION ALL
|
||||
SELECT *, 'AMAZON' AS "Quelle" FROM amazon_rows
|
||||
ORDER BY "Rechnungsdatum", "Rechnungsnummer";
|
||||
@@ -0,0 +1,439 @@
|
||||
-- =============================================================================
|
||||
-- Quartal Schweiz – Gesamt (ODOO + AMAZON), Lieferung in die Schweiz
|
||||
-- Odoo 17 / PostgreSQL
|
||||
--
|
||||
-- Zweck:
|
||||
-- Dieselben Spalten wie Alle_Rechnungen.sql (inkl. "Quelle"), aber:
|
||||
-- * Zeitraum = QUARTAL statt Monat
|
||||
-- * nur Rechnungen mit Lieferung in die SCHWEIZ (Lieferland = 'CH')
|
||||
--
|
||||
-- Modelle:
|
||||
-- ODOO : account.move (Kundenrechnungen / Gutschriften)
|
||||
-- AMAZON : brandize_datacenter_amazon_vat_transaction
|
||||
--
|
||||
-- Parameter:
|
||||
-- %(quarter)s Quartal als Text, z. B. '2026-Q3'
|
||||
-- ODOO : Rechnungsdatum im Quartal (siehe quarters-CTE)
|
||||
-- AMAZON: activity_period in den 3 Monaten des Quartals
|
||||
--
|
||||
-- Aufruf (Odoo / psycopg2):
|
||||
-- cr.execute(open('Quartal_Schweiz.sql').read(), {'quarter': '2026-Q3'})
|
||||
--
|
||||
-- In psql stattdessen:
|
||||
-- \set quarter '2026-Q3'
|
||||
--
|
||||
-- Diagnose (falls keine AMAZON-Zeilen geliefert werden):
|
||||
-- SELECT activity_period, count(*)
|
||||
-- FROM brandize_datacenter_amazon_vat_transaction
|
||||
-- WHERE arrival_country = 'CH' OR sale_arrival_country = 'CH'
|
||||
-- GROUP BY 1 ORDER BY 1;
|
||||
-- -- zeigt das tatsaechliche Format von activity_period
|
||||
--
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Spalten-Mapping ODOO -> AMAZON
|
||||
-- (Felder ohne Amazon-Pendant bleiben leer)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Rechnungsdatum <- transaction_complete_date (Fallback: depart)
|
||||
-- Rechnungsnummer <- vat_inv_number
|
||||
-- Rechnungsland <- Name zu sale_arrival_country (res_country)
|
||||
-- Rechnungsland Code <- sale_arrival_country
|
||||
-- Zahlart <- '' (kein Amazon-Feld)
|
||||
-- Zahlungsreference <- transaction_event_id
|
||||
-- USt. Id Kaeufer <- buyer_vat_number
|
||||
-- Gesamtpreis brutto <- total_activity_value_amt_vat_incl
|
||||
-- Gesamtpreis netto <- total_activity_value_amt_vat_excl
|
||||
-- Brutto / -erm <- Steuersplit nach effektivem Satz (Schwelle
|
||||
-- USt. Satz / -erm CH >= 8, sonst >= 16 - wie im ODOO-Export)
|
||||
-- Netto Betrag / -erm Satz = total_activity_value_vat_amt / netto * 100
|
||||
-- Versandkosten brutto <- total_ship_charge_amt_vat_incl
|
||||
-- Versandkosten netto <- total_ship_charge_amt_vat_excl
|
||||
-- Auftragsdatum <- transaction_depart_date
|
||||
-- Bestellnummer <- activity_transaction_id
|
||||
-- Zahldatum <- '' (kein Amazon-Feld)
|
||||
-- Lieferadresse Name <- buyer_name
|
||||
-- Tag <- '' (kein Amazon-Feld)
|
||||
-- Lieferland <- Name zu arrival_country (res_country)
|
||||
--
|
||||
-- ACHTUNG: Das Amazon-Mapping ist eine 1:1-Uebertragung auf die ODOO-Spalten.
|
||||
-- Die Datumszuordnung (Rechnungsdatum/Auftragsdatum) sowie Bestellnummer
|
||||
-- sollten gegen die tatsaechliche Amazon-Report-Semantik geprueft und hier
|
||||
-- ggf. angepasst werden.
|
||||
-- =============================================================================
|
||||
|
||||
WITH params AS (
|
||||
-- Quartal aus %(quarter)s (z. B. '2026-Q3') ableiten.
|
||||
SELECT
|
||||
split_part(%(quarter)s, '-', 1) AS yr,
|
||||
right(split_part(%(quarter)s, '-', 2), 1)::int AS qn
|
||||
),
|
||||
quarters AS (
|
||||
SELECT
|
||||
yr,
|
||||
qn,
|
||||
make_date(yr::int, (qn - 1) * 3 + 1, 1) AS first_day,
|
||||
make_date(yr::int, (qn - 1) * 3 + 1, 1) + INTERVAL '3 months' AS next_day
|
||||
FROM params
|
||||
),
|
||||
qmonths AS (
|
||||
-- Alle moeglichen Schreibweisen der 3 Quartalsmonate als 'YYYY-MMM'
|
||||
-- (deutsche UND englische Kuerzel, da die gespeicherte Schreibweise je
|
||||
-- nach Herkunft abweichen kann).
|
||||
SELECT DISTINCT
|
||||
q.yr || '-' || v.abbr AS activity_period
|
||||
FROM quarters q
|
||||
CROSS JOIN generate_series(1, 3) AS g(n)
|
||||
JOIN (VALUES
|
||||
(1, 'JAN'), (2, 'FEB'), (3, 'MÄR'), (3, 'MAR'), (4, 'APR'),
|
||||
(5, 'MAI'), (5, 'MAY'), (6, 'JUN'), (7, 'JUL'), (8, 'AUG'),
|
||||
(9, 'SEP'), (10, 'OKT'), (10, 'OCT'), (11, 'NOV'),
|
||||
(12, 'DEZ'), (12, 'DEC')
|
||||
) AS v(mnum, abbr) ON v.mnum = (q.qn - 1) * 3 + g.n
|
||||
),
|
||||
|
||||
-- ===========================================================================
|
||||
-- ODOO-Seite (identisch zu Rechnungsexport.sql)
|
||||
-- ===========================================================================
|
||||
|
||||
invoices AS (
|
||||
SELECT
|
||||
am.id,
|
||||
am.name,
|
||||
am.ref,
|
||||
am.move_type,
|
||||
am.invoice_date,
|
||||
am.invoice_origin,
|
||||
am.partner_id,
|
||||
am.partner_shipping_id,
|
||||
am.invoice_payment_term_id,
|
||||
am.payment_reference
|
||||
FROM account_move am
|
||||
CROSS JOIN quarters q
|
||||
WHERE am.invoice_date >= q.first_day
|
||||
AND am.invoice_date < q.next_day
|
||||
AND am.move_type IN ('out_invoice', 'out_refund')
|
||||
),
|
||||
|
||||
line_taxes AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
at.amount AS tax_rate,
|
||||
SUM(aml.price_subtotal) AS netto,
|
||||
SUM(aml.price_total) AS brutto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_move_line_account_tax_rel amlatr
|
||||
ON amlatr.account_move_line_id = aml.id
|
||||
JOIN account_tax at
|
||||
ON at.id = amlatr.account_tax_id
|
||||
GROUP BY aml.move_id, at.amount
|
||||
),
|
||||
|
||||
tax_split AS (
|
||||
SELECT
|
||||
lt.invoice_id,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_erm
|
||||
|
||||
FROM line_taxes lt
|
||||
JOIN invoices inv
|
||||
ON inv.id = lt.invoice_id
|
||||
JOIN res_partner rp
|
||||
ON rp.id = inv.partner_id
|
||||
LEFT JOIN res_country rc
|
||||
ON rc.id = rp.country_id
|
||||
GROUP BY lt.invoice_id
|
||||
),
|
||||
|
||||
shipping AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
ROUND(SUM(aml.price_total)::numeric, 2) AS shipping_brutto,
|
||||
ROUND(SUM(aml.price_subtotal)::numeric, 2) AS shipping_netto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN product_product pp
|
||||
ON pp.id = aml.product_id
|
||||
JOIN product_template pt
|
||||
ON pt.id = pp.product_tmpl_id
|
||||
WHERE pt.categ_id = 9
|
||||
GROUP BY aml.move_id
|
||||
),
|
||||
|
||||
invoice_tags AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
STRING_AGG(
|
||||
DISTINCT COALESCE(ct.name->>'en_US', ct.name->>'de_DE', ''),
|
||||
', ' ORDER BY COALESCE(ct.name->>'en_US', ct.name->>'de_DE', '')
|
||||
) AS tags
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
JOIN sale_order_tag_rel sotr
|
||||
ON sotr.order_id = so.id
|
||||
JOIN crm_tag ct
|
||||
ON ct.id = sotr.tag_id
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
invoice_transaction_reference AS (
|
||||
SELECT
|
||||
rel.invoice_id,
|
||||
pt.provider_reference,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY rel.invoice_id
|
||||
ORDER BY pt.id
|
||||
) AS rn
|
||||
FROM account_invoice_transaction_rel rel
|
||||
JOIN payment_transaction pt
|
||||
ON pt.id = rel.transaction_id
|
||||
),
|
||||
|
||||
sale_info AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
MIN(so.date_order) AS order_date
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
invoice_payments AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
MAX(apr.max_date) AS payment_date
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_account aa
|
||||
ON aa.id = aml.account_id
|
||||
AND aa.account_type = 'asset_receivable'
|
||||
JOIN account_partial_reconcile apr
|
||||
ON apr.debit_move_id = aml.id
|
||||
OR apr.credit_move_id = aml.id
|
||||
GROUP BY aml.move_id
|
||||
),
|
||||
|
||||
odoo_rows AS (
|
||||
SELECT
|
||||
TO_CHAR(inv.invoice_date, 'YYYY-MM-DD') AS "Rechnungsdatum",
|
||||
|
||||
CASE
|
||||
WHEN inv.move_type = 'out_refund'
|
||||
THEN COALESCE(inv.name, inv.ref, '') || '-gs'
|
||||
ELSE COALESCE(inv.name, inv.ref, '')
|
||||
END AS "Rechnungsnummer",
|
||||
|
||||
COALESCE(rc_inv.name->>'en_US', rc_inv.name->>'de_DE', '') AS "Rechnungsland",
|
||||
COALESCE(rc_inv.code, '') AS "Rechnungsland Ländercode",
|
||||
COALESCE(apt.name->>'en_US', apt.name->>'de_DE', '') AS "Zahlart",
|
||||
|
||||
COALESCE(itr.provider_reference, '') AS "Zahlungsreference der Bestellung",
|
||||
COALESCE(partner.vat, '') AS "USt. Id / VAT Id des Käufers (in Bestellung)",
|
||||
|
||||
ROUND(COALESCE(ts.brutto_normal, 0) + COALESCE(ts.brutto_erm, 0), 2) AS "Gesamtpreis brutto",
|
||||
ROUND(COALESCE(ts.netto_normal, 0) + COALESCE(ts.netto_erm, 0), 2) AS "Gesamtpreis netto",
|
||||
|
||||
COALESCE(ts.brutto_normal, 0) AS "Brutto",
|
||||
COALESCE(ts.brutto_erm, 0) AS "Brutto erm",
|
||||
COALESCE(ts.ust_satz_normal, 0) AS "USt. Satz",
|
||||
COALESCE(ts.ust_satz_erm, 0) AS "Ust. Satz erm",
|
||||
COALESCE(ts.netto_normal, 0) AS "Netto Betrag",
|
||||
COALESCE(ts.netto_erm, 0) AS "Netto Betrag erm",
|
||||
|
||||
COALESCE(sh.shipping_brutto, 0) AS "Versandkosten brutto",
|
||||
COALESCE(sh.shipping_netto, 0) AS "Versandkosten netto",
|
||||
|
||||
COALESCE(TO_CHAR(si.order_date, 'YYYY-MM-DD'), '') AS "Auftragsdatum",
|
||||
COALESCE(inv.invoice_origin, '') AS "Bestellnummer",
|
||||
COALESCE(TO_CHAR(ip.payment_date, 'YYYY-MM-DD'), '') AS "Zahldatum",
|
||||
|
||||
COALESCE(ship.name, partner.name, '') AS "Lieferadresse Name",
|
||||
COALESCE(it.tags, '') AS "Tag",
|
||||
COALESCE(
|
||||
rc_ship.name->>'en_US', rc_ship.name->>'de_DE',
|
||||
rc_inv.name->>'en_US', rc_inv.name->>'de_DE',
|
||||
''
|
||||
) AS "Lieferland"
|
||||
|
||||
FROM invoices inv
|
||||
JOIN res_partner partner
|
||||
ON partner.id = inv.partner_id
|
||||
LEFT JOIN res_country rc_inv
|
||||
ON rc_inv.id = partner.country_id
|
||||
LEFT JOIN res_partner ship
|
||||
ON ship.id = inv.partner_shipping_id
|
||||
LEFT JOIN res_country rc_ship
|
||||
ON rc_ship.id = ship.country_id
|
||||
LEFT JOIN account_payment_term apt
|
||||
ON apt.id = inv.invoice_payment_term_id
|
||||
LEFT JOIN tax_split ts
|
||||
ON ts.invoice_id = inv.id
|
||||
LEFT JOIN shipping sh
|
||||
ON sh.invoice_id = inv.id
|
||||
LEFT JOIN invoice_tags it
|
||||
ON it.invoice_id = inv.id
|
||||
LEFT JOIN invoice_transaction_reference itr
|
||||
ON itr.invoice_id = inv.id
|
||||
AND itr.rn = 1
|
||||
LEFT JOIN sale_info si
|
||||
ON si.invoice_id = inv.id
|
||||
LEFT JOIN invoice_payments ip
|
||||
ON ip.invoice_id = inv.id
|
||||
-- Nur Lieferungen in die Schweiz (Versandland, Fallback: Rechnungsland).
|
||||
WHERE COALESCE(rc_ship.code, rc_inv.code) = 'CH'
|
||||
),
|
||||
|
||||
-- ===========================================================================
|
||||
-- AMAZON-Seite
|
||||
-- ===========================================================================
|
||||
|
||||
-- Basiszahlen + effektiver USt-Satz je Transaktionszeile.
|
||||
amazon_base AS (
|
||||
SELECT
|
||||
a.transaction_complete_date,
|
||||
a.transaction_depart_date,
|
||||
a.vat_inv_number,
|
||||
a.sale_arrival_country,
|
||||
a.arrival_country,
|
||||
a.transaction_event_id,
|
||||
a.buyer_vat_number,
|
||||
a.buyer_name,
|
||||
a.activity_transaction_id,
|
||||
COALESCE(a.total_activity_value_amt_vat_incl, 0) AS gross,
|
||||
COALESCE(a.total_activity_value_amt_vat_excl, 0) AS net,
|
||||
COALESCE(a.total_ship_charge_amt_vat_incl, 0) AS ship_gross,
|
||||
COALESCE(a.total_ship_charge_amt_vat_excl, 0) AS ship_net,
|
||||
CASE
|
||||
WHEN COALESCE(a.total_activity_value_amt_vat_excl, 0) <> 0
|
||||
THEN ROUND((COALESCE(a.total_activity_value_vat_amt, 0)
|
||||
/ a.total_activity_value_amt_vat_excl * 100)::numeric, 2)
|
||||
ELSE 0
|
||||
END AS rate
|
||||
FROM brandize_datacenter_amazon_vat_transaction a
|
||||
WHERE upper(a.activity_period) IN (SELECT upper(activity_period) FROM qmonths)
|
||||
-- Lieferung in die Schweiz: Ziel-/Versandland kann in arrival_country
|
||||
-- oder sale_arrival_country stehen.
|
||||
AND (upper(a.arrival_country) = 'CH' OR upper(a.sale_arrival_country) = 'CH')
|
||||
-- Optional: Gebuehrenzeilen ausschliessen (wie in VAT_Summary.sql):
|
||||
-- AND a.sales_channel <> 'AMAZON_FEE'
|
||||
),
|
||||
|
||||
amazon_rows AS (
|
||||
SELECT
|
||||
TO_CHAR(COALESCE(b.transaction_complete_date, b.transaction_depart_date), 'YYYY-MM-DD')
|
||||
AS "Rechnungsdatum",
|
||||
COALESCE(b.vat_inv_number, '') AS "Rechnungsnummer",
|
||||
COALESCE(rc_inv.name->>'en_US', rc_inv.name->>'de_DE', '') AS "Rechnungsland",
|
||||
COALESCE(b.sale_arrival_country, '') AS "Rechnungsland Ländercode",
|
||||
'' AS "Zahlart",
|
||||
COALESCE(b.transaction_event_id, '') AS "Zahlungsreference der Bestellung",
|
||||
COALESCE(b.buyer_vat_number, '') AS "USt. Id / VAT Id des Käufers (in Bestellung)",
|
||||
|
||||
ROUND(b.gross::numeric, 2) AS "Gesamtpreis brutto",
|
||||
ROUND(b.net::numeric, 2) AS "Gesamtpreis netto",
|
||||
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN ROUND(b.gross::numeric, 2) ELSE 0
|
||||
END AS "Brutto",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN ROUND(b.gross::numeric, 2) ELSE 0
|
||||
END AS "Brutto erm",
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN b.rate ELSE 0
|
||||
END AS "USt. Satz",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN b.rate ELSE 0
|
||||
END AS "Ust. Satz erm",
|
||||
CASE
|
||||
WHEN (b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
THEN ROUND(b.net::numeric, 2) ELSE 0
|
||||
END AS "Netto Betrag",
|
||||
CASE
|
||||
WHEN NOT (
|
||||
(b.sale_arrival_country = 'CH' AND b.rate >= 8)
|
||||
OR (COALESCE(b.sale_arrival_country, '') <> 'CH' AND b.rate >= 16)
|
||||
)
|
||||
THEN ROUND(b.net::numeric, 2) ELSE 0
|
||||
END AS "Netto Betrag erm",
|
||||
|
||||
ROUND(b.ship_gross::numeric, 2) AS "Versandkosten brutto",
|
||||
ROUND(b.ship_net::numeric, 2) AS "Versandkosten netto",
|
||||
|
||||
COALESCE(TO_CHAR(b.transaction_depart_date, 'YYYY-MM-DD'), '') AS "Auftragsdatum",
|
||||
COALESCE(b.activity_transaction_id, '') AS "Bestellnummer",
|
||||
'' AS "Zahldatum",
|
||||
COALESCE(b.buyer_name, '') AS "Lieferadresse Name",
|
||||
'' AS "Tag",
|
||||
COALESCE(rc_ship.name->>'en_US', rc_ship.name->>'de_DE', '') AS "Lieferland"
|
||||
|
||||
FROM amazon_base b
|
||||
LEFT JOIN res_country rc_inv
|
||||
ON rc_inv.code = b.sale_arrival_country
|
||||
LEFT JOIN res_country rc_ship
|
||||
ON rc_ship.code = b.arrival_country
|
||||
)
|
||||
|
||||
-- ===========================================================================
|
||||
-- Zusammenfuehrung: ODOO + AMAZON, je Zeile mit Quelle
|
||||
-- ===========================================================================
|
||||
SELECT *, 'ODOO' AS "Quelle" FROM odoo_rows
|
||||
UNION ALL
|
||||
SELECT *, 'AMAZON' AS "Quelle" FROM amazon_rows
|
||||
ORDER BY "Rechnungsdatum", "Rechnungsnummer";
|
||||
@@ -0,0 +1,292 @@
|
||||
-- =============================================================================
|
||||
-- Rechnungsexport – Odoo 17 / PostgreSQL
|
||||
-- Rechnungsdatum · Rechnungsnummer · Länder · Zahlart · USt-Nr. · Steuersplit
|
||||
--
|
||||
-- Modell : account.move (Kundenrechnungen und Gutschriften)
|
||||
--
|
||||
-- Parameter:
|
||||
-- %(month)s Abrechnungszeitraum als Text, z. B. '2026-SEP'
|
||||
--
|
||||
-- Aufruf (Odoo / psycopg2):
|
||||
-- cr.execute(open('Rechnungsexport.sql').read(), {'month': '2026-SEP'})
|
||||
--
|
||||
-- In psql stattdessen:
|
||||
-- \set month '2026-SEP'
|
||||
-- ... AND am.invoice_date >= :'month' (siehe params-CTE)
|
||||
--
|
||||
-- Hinweis zum Monatsfilter:
|
||||
-- Das Rechnungsdatum wird auf einen vollen Kalendermonat gefiltert.
|
||||
-- Der Monat wird locale-unabhängig in ein Datum umgerechnet; es werden
|
||||
-- deutsche (MÄR, MAI, OKT, DEZ) und englische (MAR, MAY, OCT, DEC)
|
||||
-- Dreibuchstaben-Kürzel akzeptiert.
|
||||
--
|
||||
-- Änderungen gegenüber der Vorversion (nur die fehlenden Felder ergänzt):
|
||||
-- * Auftragsdatum : aus sale_order.date_order (über invoice_origin)
|
||||
-- * Zahldatum : aus der letzten Ausgleichsbuchung der Debitorenzeile
|
||||
-- * Lieferadresse Name / Lieferland : aus account_move.partner_shipping_id
|
||||
-- (Fallback: Rechnungspartner)
|
||||
-- =============================================================================
|
||||
|
||||
WITH params AS (
|
||||
-- Kalendermonat aus %(month)s (z. B. '2026-SEP') ableiten.
|
||||
SELECT
|
||||
to_date(
|
||||
split_part(%(month)s, '-', 1) || '-' ||
|
||||
CASE upper(split_part(%(month)s, '-', 2))
|
||||
WHEN 'JAN' THEN '01' WHEN 'FEB' THEN '02'
|
||||
WHEN 'MÄR' THEN '03' WHEN 'MAR' THEN '03' WHEN 'APR' THEN '04'
|
||||
WHEN 'MAI' THEN '05' WHEN 'MAY' THEN '05' WHEN 'JUN' THEN '06'
|
||||
WHEN 'JUL' THEN '07' WHEN 'AUG' THEN '08' WHEN 'SEP' THEN '09'
|
||||
WHEN 'OKT' THEN '10' WHEN 'OCT' THEN '10' WHEN 'NOV' THEN '11'
|
||||
WHEN 'DEZ' THEN '12' WHEN 'DEC' THEN '12'
|
||||
END,
|
||||
'YYYY-MM'
|
||||
) AS first_day
|
||||
),
|
||||
|
||||
invoices AS (
|
||||
SELECT
|
||||
am.id,
|
||||
am.name,
|
||||
am.ref,
|
||||
am.move_type,
|
||||
am.invoice_date,
|
||||
am.invoice_origin,
|
||||
am.partner_id,
|
||||
am.partner_shipping_id,
|
||||
am.invoice_payment_term_id,
|
||||
am.payment_reference
|
||||
FROM account_move am
|
||||
CROSS JOIN params p
|
||||
WHERE am.invoice_date >= p.first_day
|
||||
AND am.invoice_date < p.first_day + INTERVAL '1 month'
|
||||
AND am.move_type IN ('out_invoice', 'out_refund')
|
||||
),
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- tax summary: invoice line -> tax relation -> account_tax
|
||||
-- ---------------------------------------------------------------------------
|
||||
line_taxes AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
at.amount AS tax_rate,
|
||||
SUM(aml.price_subtotal) AS netto,
|
||||
SUM(aml.price_total) AS brutto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_move_line_account_tax_rel amlatr
|
||||
ON amlatr.account_move_line_id = aml.id
|
||||
JOIN account_tax at
|
||||
ON at.id = amlatr.account_tax_id
|
||||
GROUP BY aml.move_id, at.amount
|
||||
),
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- tax split logic from Python:
|
||||
-- threshold = 8 for CH, else 16
|
||||
-- ---------------------------------------------------------------------------
|
||||
tax_split AS (
|
||||
SELECT
|
||||
lt.invoice_id,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN (rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_normal,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.brutto::numeric, 2)
|
||||
END) AS brutto_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.tax_rate::numeric, 2)
|
||||
END) AS ust_satz_erm,
|
||||
|
||||
MAX(CASE
|
||||
WHEN NOT (
|
||||
(rc.code = 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 8)
|
||||
OR (COALESCE(rc.code, '') <> 'CH' AND ROUND(lt.tax_rate::numeric, 2) >= 16)
|
||||
)
|
||||
THEN ROUND(lt.netto::numeric, 2)
|
||||
END) AS netto_erm
|
||||
|
||||
FROM line_taxes lt
|
||||
JOIN invoices inv
|
||||
ON inv.id = lt.invoice_id
|
||||
JOIN res_partner rp
|
||||
ON rp.id = inv.partner_id
|
||||
LEFT JOIN res_country rc
|
||||
ON rc.id = rp.country_id
|
||||
GROUP BY lt.invoice_id
|
||||
),
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- shipping costs (product category id = 9, as in Python logic)
|
||||
-- ---------------------------------------------------------------------------
|
||||
shipping AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
ROUND(SUM(aml.price_total)::numeric, 2) AS shipping_brutto,
|
||||
ROUND(SUM(aml.price_subtotal)::numeric, 2) AS shipping_netto
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN product_product pp
|
||||
ON pp.id = aml.product_id
|
||||
JOIN product_template pt
|
||||
ON pt.id = pp.product_tmpl_id
|
||||
WHERE pt.categ_id = 9
|
||||
GROUP BY aml.move_id
|
||||
),
|
||||
|
||||
invoice_tags AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
STRING_AGG(
|
||||
DISTINCT COALESCE(ct.name->>'en_US', ct.name->>'de_DE', ''),
|
||||
', ' ORDER BY COALESCE(ct.name->>'en_US', ct.name->>'de_DE', '')
|
||||
) AS tags
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
JOIN sale_order_tag_rel sotr
|
||||
ON sotr.order_id = so.id
|
||||
JOIN crm_tag ct
|
||||
ON ct.id = sotr.tag_id
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
invoice_transaction_reference AS (
|
||||
SELECT
|
||||
rel.invoice_id,
|
||||
pt.provider_reference,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY rel.invoice_id
|
||||
ORDER BY pt.id
|
||||
) AS rn
|
||||
FROM account_invoice_transaction_rel rel
|
||||
JOIN payment_transaction pt
|
||||
ON pt.id = rel.transaction_id
|
||||
),
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Auftragsdatum: Bestellung über invoice_origin (Verkaufsauftragsname) finden
|
||||
-- ---------------------------------------------------------------------------
|
||||
sale_info AS (
|
||||
SELECT
|
||||
inv.id AS invoice_id,
|
||||
MIN(so.date_order) AS order_date
|
||||
FROM invoices inv
|
||||
JOIN sale_order so
|
||||
ON so.name = inv.invoice_origin
|
||||
GROUP BY inv.id
|
||||
),
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Zahldatum: Datum der letzten Ausgleichsbuchung der Debitorenzeile.
|
||||
-- account_partial_reconcile.max_date = jüngeres Datum der beiden Buchungen.
|
||||
-- ---------------------------------------------------------------------------
|
||||
invoice_payments AS (
|
||||
SELECT
|
||||
aml.move_id AS invoice_id,
|
||||
MAX(apr.max_date) AS payment_date
|
||||
FROM invoices inv
|
||||
JOIN account_move_line aml
|
||||
ON aml.move_id = inv.id
|
||||
JOIN account_account aa
|
||||
ON aa.id = aml.account_id
|
||||
AND aa.account_type = 'asset_receivable'
|
||||
JOIN account_partial_reconcile apr
|
||||
ON apr.debit_move_id = aml.id
|
||||
OR apr.credit_move_id = aml.id
|
||||
GROUP BY aml.move_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
TO_CHAR(inv.invoice_date, 'YYYY-MM-DD') AS "Rechnungsdatum",
|
||||
|
||||
CASE
|
||||
WHEN inv.move_type = 'out_refund'
|
||||
THEN COALESCE(inv.name, inv.ref, '') || '-gs'
|
||||
ELSE COALESCE(inv.name, inv.ref, '')
|
||||
END AS "Rechnungsnummer",
|
||||
|
||||
COALESCE(rc_inv.name->>'en_US', rc_inv.name->>'de_DE', '') AS "Rechnungsland",
|
||||
COALESCE(rc_inv.code, '') AS "Rechnungsland Ländercode",
|
||||
COALESCE(apt.name->>'en_US', apt.name->>'de_DE', '') AS "Zahlart",
|
||||
|
||||
COALESCE(itr.provider_reference, '') AS "Zahlungsreference der Bestellung",
|
||||
COALESCE(partner.vat, '') AS "USt. Id / VAT Id des Käufers (in Bestellung)",
|
||||
|
||||
ROUND(COALESCE(ts.brutto_normal, 0) + COALESCE(ts.brutto_erm, 0), 2) AS "Gesamtpreis brutto",
|
||||
ROUND(COALESCE(ts.netto_normal, 0) + COALESCE(ts.netto_erm, 0), 2) AS "Gesamtpreis netto",
|
||||
|
||||
COALESCE(ts.brutto_normal, 0) AS "Brutto",
|
||||
COALESCE(ts.brutto_erm, 0) AS "Brutto erm",
|
||||
COALESCE(ts.ust_satz_normal, 0) AS "USt. Satz",
|
||||
COALESCE(ts.ust_satz_erm, 0) AS "Ust. Satz erm",
|
||||
COALESCE(ts.netto_normal, 0) AS "Netto Betrag",
|
||||
COALESCE(ts.netto_erm, 0) AS "Netto Betrag erm",
|
||||
|
||||
COALESCE(sh.shipping_brutto, 0) AS "Versandkosten brutto",
|
||||
COALESCE(sh.shipping_netto, 0) AS "Versandkosten netto",
|
||||
|
||||
COALESCE(TO_CHAR(si.order_date, 'YYYY-MM-DD'), '') AS "Auftragsdatum",
|
||||
COALESCE(inv.invoice_origin, '') AS "Bestellnummer",
|
||||
COALESCE(TO_CHAR(ip.payment_date, 'YYYY-MM-DD'), '') AS "Zahldatum",
|
||||
|
||||
COALESCE(ship.name, partner.name, '') AS "Lieferadresse Name",
|
||||
COALESCE(it.tags, '') AS "Tag",
|
||||
COALESCE(
|
||||
rc_ship.name->>'en_US', rc_ship.name->>'de_DE',
|
||||
rc_inv.name->>'en_US', rc_inv.name->>'de_DE',
|
||||
''
|
||||
) AS "Lieferland"
|
||||
|
||||
FROM invoices inv
|
||||
JOIN res_partner partner
|
||||
ON partner.id = inv.partner_id
|
||||
LEFT JOIN res_country rc_inv
|
||||
ON rc_inv.id = partner.country_id
|
||||
LEFT JOIN res_partner ship
|
||||
ON ship.id = inv.partner_shipping_id
|
||||
LEFT JOIN res_country rc_ship
|
||||
ON rc_ship.id = ship.country_id
|
||||
LEFT JOIN account_payment_term apt
|
||||
ON apt.id = inv.invoice_payment_term_id
|
||||
LEFT JOIN tax_split ts
|
||||
ON ts.invoice_id = inv.id
|
||||
LEFT JOIN shipping sh
|
||||
ON sh.invoice_id = inv.id
|
||||
LEFT JOIN invoice_tags it
|
||||
ON it.invoice_id = inv.id
|
||||
LEFT JOIN invoice_transaction_reference itr
|
||||
ON itr.invoice_id = inv.id
|
||||
AND itr.rn = 1
|
||||
LEFT JOIN sale_info si
|
||||
ON si.invoice_id = inv.id
|
||||
LEFT JOIN invoice_payments ip
|
||||
ON ip.invoice_id = inv.id
|
||||
ORDER BY inv.invoice_date, inv.name;
|
||||
@@ -0,0 +1,111 @@
|
||||
# Table: `brandize_datacenter_amazon_inventory_detailed`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.inventory.detailed`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Amazon Inventory Ledger Detailed - Detailed inventory ledger entries
|
||||
**Type:** Basisobjekt (Base object)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This table stores detailed inventory ledger entries for Amazon FBA (Fulfillment by Amazon). Each record represents a specific inventory event or transaction, providing a granular view of inventory movements including receipts, shipments, adjustments, and reconciliations.
|
||||
|
||||
---
|
||||
|
||||
## Fields
|
||||
|
||||
### Core Identifier Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `id` | `id` | Ganzzahl (Integer) | Primary key / Record ID |
|
||||
| `display_name` | `display_name` | Zeichen (Char) | Display name of the record |
|
||||
|
||||
### Activity Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `create_date` | `create_date` | Datum/Zeit (Datetime) | Created on - Timestamp when record was created |
|
||||
| `create_uid` | `create_uid` | Many2One | Created by - Reference to user who created the record |
|
||||
| `write_date` | `write_date` | Datum/Zeit (Datetime) | Last Updated on - Timestamp of last modification |
|
||||
| `write_uid` | `write_uid` | Many2One | Last Updated by - Reference to user who last modified the record |
|
||||
|
||||
### Inventory Identification
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `asin` | `asin` | Zeichen (Char) | Amazon Standard Identification Number |
|
||||
| `fnsku` | `fnsku` | Zeichen (Char) | Amazon FNSKU (Fulfillment Network Stock Keeping Unit) |
|
||||
| `msku` | `msku` | Zeichen (Char) | Merchant SKU (Seller's internal SKU) |
|
||||
| `title` | `title` | Zeichen (Char) | Product title |
|
||||
|
||||
### Event Details
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `date` | `date` | Datum (Date) | Date of the inventory event |
|
||||
| `date_time` | `date_time` | Datum/Zeit (Datetime) | Date and time of the inventory event |
|
||||
| `event_type` | `event_type` | Zeichen (Char) | Type of inventory event (e.g., receipt, shipment, adjustment) |
|
||||
| `reason` | `reason` | Zeichen (Char) | Reason for the event |
|
||||
| `reference_id` | `reference_id` | Zeichen (Char) | Reference ID (e.g., order number, shipment ID) |
|
||||
|
||||
### Location
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `country` | `country` | Zeichen (Char) | Country |
|
||||
| `fulfillment_center` | `fulfillment_center` | Zeichen (Char) | Fulfillment center location |
|
||||
| `disposition` | `disposition` | Zeichen (Char) | Inventory disposition/condition status |
|
||||
|
||||
### Quantity Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `quantity` | `quantity` | Ganzzahl (Integer) | Quantity of items in this event |
|
||||
| `reconciled_quantity` | `reconciled_quantity` | Ganzzahl (Integer) | Reconciled quantity (verified count) |
|
||||
| `unreconciled_quantity` | `unreconciled_quantity` | Ganzzahl (Integer) | Unreconciled quantity (pending verification) |
|
||||
|
||||
### Financial Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `inventory_position_value` | `inventory_position_value` | Gleitkommazahl (Float) | Inventory position value |
|
||||
| `helper_cogs` | `helper_cogs` | Gleitkommazahl (Float) | Helper Column: COGS (Cost of Goods Sold) |
|
||||
|
||||
---
|
||||
|
||||
## Database Indexes
|
||||
|
||||
The table likely has indexes on:
|
||||
- `id` (Primary Key)
|
||||
- `create_date` / `write_date` (for audit tracking)
|
||||
- `date` / `date_time` (for date-based filtering)
|
||||
- `asin` (for product lookups)
|
||||
- `fnsku` / `msku` (for SKU lookups)
|
||||
- `fulfillment_center` (for warehouse-specific queries)
|
||||
- `event_type` (for event type filtering)
|
||||
|
||||
---
|
||||
|
||||
## Usage in BI Dashboard
|
||||
|
||||
This table can be used for:
|
||||
|
||||
- **Detailed inventory movement analysis** - Tracking each inventory event with full details
|
||||
- **Reconciliation reporting** - Comparing reconciled vs unreconciled quantities
|
||||
- **COGS tracking** - Monitoring cost of goods sold at the event level via `helper_cogs`
|
||||
- **Inventory valuation** - Using `inventory_position_value` for position-based valuation
|
||||
- **Fulfillment center analysis** - Tracking inventory by specific fulfillment centers
|
||||
- **Event type reporting** - Analyzing different types of inventory events
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This table provides more granular detail than the summary table (`brandize_datacenter_amazon_inventory_summary`)
|
||||
- The `date` field is stored as a proper DATE type (unlike the summary table where it's a character string)
|
||||
- `date_time` provides timestamp precision for event ordering
|
||||
- `reconciled_quantity` vs `unreconciled_quantity` allows tracking inventory verification status
|
||||
- `helper_cogs` provides calculated COGS at the event level for detailed cost tracking
|
||||
- Multiple events for the same product can be tracked with different dates, reasons, and fulfillment centers
|
||||
@@ -0,0 +1,124 @@
|
||||
# Table: `brandize_datacenter_amazon_inventory_summary`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.inventory.summary`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Amazon Inventory Ledger Summary - Inventory ledger and stock level tracking
|
||||
**Type:** Basisobjekt (Base object)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This table stores Amazon inventory ledger data, tracking inventory movements including shipments, returns, damages, and warehouse transfers. It provides visibility into inventory levels, movements, and valuation.
|
||||
|
||||
---
|
||||
|
||||
## Fields
|
||||
|
||||
### Core Identifier Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `id` | `id` | Ganzzahl (Integer) | Primary key / Record ID |
|
||||
| `display_name` | `display_name` | Zeichen (Char) | Display name of the record |
|
||||
|
||||
### Activity Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `create_date` | `create_date` | Datum/Zeit (Datetime) | Created on - Timestamp when record was created |
|
||||
| `create_uid` | `create_uid` | Many2One | Created by - Reference to user who created the record |
|
||||
| `write_date` | `write_date` | Datum/Zeit (Datetime) | Last Updated on - Timestamp of last modification |
|
||||
| `write_uid` | `write_uid` | Many2One | Last Updated by - Reference to user who last modified the record |
|
||||
|
||||
### Inventory Identification
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `asin` | `asin` | Zeichen (Char) | Amazon Standard Identification Number |
|
||||
| `fnsku` | `fnsku` | Zeichen (Char) | Amazon FNSKU (Fulfillment Network Stock Keeping Unit) |
|
||||
| `msku` | `msku` | Zeichen (Char) | Merchant SKU (Seller's internal SKU) |
|
||||
| `title` | `title` | Zeichen (Char) | Product title |
|
||||
| `date` | `date` | Zeichen (Char) | Date of the inventory record |
|
||||
|
||||
### Inventory Balances
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `starting_warehouse_balance` | `starting_warehouse_balance` | Ganzzahl (Integer) | Starting warehouse balance (beginning inventory) |
|
||||
| `ending_warehouse_balance` | `ending_warehouse_balance` | Ganzzahl (Integer) | Ending warehouse balance (final inventory) |
|
||||
|
||||
### Inventory Movements
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `customer_shipments` | `customer_shipments` | Ganzzahl (Integer) | Number of customer shipments (sales) |
|
||||
| `receipts` | `receipts` | Ganzzahl (Integer) | Number of receipts (incoming stock) |
|
||||
| `customer_returns` | `customer_returns` | Ganzzahl (Integer) | Number of customer returns |
|
||||
| `vendor_returns` | `vendor_returns` | Ganzzahl (Integer) | Number of vendor returns |
|
||||
| `warehouse_transfer_in_out` | `warehouse_transfer_in_out` | Ganzzahl (Integer) | Warehouse transfer in/out |
|
||||
|
||||
### Inventory Condition Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `damaged` | `damaged` | Ganzzahl (Integer) | Number of damaged items |
|
||||
| `disposed` | `disposed` | Ganzzahl (Integer) | Number of disposed items |
|
||||
| `lost` | `lost` | Ganzzahl (Integer) | Number of lost items |
|
||||
| `found` | `found` | Ganzzahl (Integer) | Number of found items |
|
||||
|
||||
### Other Events
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `in_transit_between_warehouses` | `in_transit_between_warehouses` | Ganzzahl (Integer) | Items in transit between warehouses |
|
||||
| `other_events` | `other_events` | Ganzzahl (Integer) | Other inventory events |
|
||||
| `unknown_events` | `unknown_events` | Ganzzahl (Integer) | Unknown/uncategorized events |
|
||||
|
||||
### Location
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `location` | `location` | Zeichen (Char) | Warehouse location |
|
||||
| `disposition` | `disposition` | Zeichen (Char) | Inventory disposition/condition status |
|
||||
|
||||
### Helper Fields (Valuation)
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `helper_cogs` | `helper_cogs` | Gleitkommazahl (Float) | Helper Column: COGS (Cost of Goods Sold) |
|
||||
| `helper_starting_inventory_value` | `helper_starting_inventory_value` | Gleitkommazahl (Float) | Helper Column: Starting Inventory Value |
|
||||
| `helper_ending_inventory_value` | `helper_ending_inventory_value` | Gleitkommazahl (Float) | Helper Column: Ending Inventory Value |
|
||||
|
||||
---
|
||||
|
||||
## Database Indexes
|
||||
|
||||
The table likely has indexes on:
|
||||
- `id` (Primary Key)
|
||||
- `create_date` / `write_date` (for audit tracking)
|
||||
- `asin` (for product lookups)
|
||||
- `fnsku` / `msku` (for SKU lookups)
|
||||
- `date` (for date-based filtering)
|
||||
- `location` (for warehouse-specific queries)
|
||||
|
||||
---
|
||||
|
||||
## Usage in BI Dashboard
|
||||
|
||||
This table can be used for:
|
||||
|
||||
- **Inventory valuation reports** - Using helper fields for starting/ending inventory values
|
||||
- **COGS tracking** - Monitoring cost of goods sold via `helper_cogs`
|
||||
- **Inventory movement analysis** - Tracking shipments, returns, and transfers
|
||||
- **Damage/loss reporting** - Monitoring damaged, lost, or disposed items
|
||||
- **Warehouse balance reporting** - Starting and ending inventory levels per location
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The `date` field is stored as a character string (not a proper DATE type) - may require casting in queries
|
||||
- Helper columns (`helper_cogs`, `helper_starting_inventory_value`, `helper_ending_inventory_value`) appear to be calculated values for inventory valuation
|
||||
- The `disposition` field likely indicates inventory condition status (e.g., sellable, unsellable, damaged)
|
||||
- Inventory movements are tracked as discrete counts rather than running balances
|
||||
@@ -0,0 +1,285 @@
|
||||
# Table: `brandize_datacenter_amazon_vat_transaction`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.vat.transaction`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Auftrag (Order/Transaction) - Primary table for Amazon VAT transaction data
|
||||
**Type:** Basisobjekt (Base object)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This table stores all Amazon VAT transaction data for the Brandize data center. It contains comprehensive information about orders including shipping details, tax calculations, buyer/seller information, and financial data.
|
||||
|
||||
---
|
||||
|
||||
## Fields
|
||||
|
||||
### Core Identifier Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `id` | `id` | Ganzzahl (Integer) | Primary key / Record ID |
|
||||
| `display_name` | `display_name` | Zeichen (Char) | Display name of the record |
|
||||
|
||||
### Activity Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `create_date` | `create_date` | Datum/Zeit (Datetime) | Created on - Timestamp when record was created |
|
||||
| `create_uid` | `create_uid` | Many2One | Created by - Reference to user who created the record |
|
||||
| `write_date` | `write_date` | Datum/Zeit (Datetime) | Last Updated on - Timestamp of last modification |
|
||||
| `write_uid` | `write_uid` | Many2One | Last Updated by - Reference to user who last modified the record |
|
||||
|
||||
### Transaction Identification
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `transaction_event_id` | `transaction_event_id` | Zeichen (Char) | Unique transaction event identifier |
|
||||
| `transaction_type` | `transaction_type` | Zeichen (Char) | Type of transaction |
|
||||
| `transaction_currency_code` | `transaction_currency_code` | Zeichen (Char) | Currency code for the transaction |
|
||||
| `unique_account_identifier` | `unique_account_identifier` | Zeichen (Char) | Unique identifier for the account |
|
||||
|
||||
### Date Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `transaction_depart_date` | `transaction_depart_date` | Datum (Date) | Date of departure |
|
||||
| `transaction_arrival_date` | `transaction_arrival_date` | Datum (Date) | Date of arrival |
|
||||
| `transaction_complete_date` | `transaction_complete_date` | Datum (Date) | Date when transaction was completed |
|
||||
| `tax_calculation_date` | `tax_calculation_date` | Datum (Date) | Date when tax was calculated |
|
||||
| `vat_inv_exchange_rate_date` | `vat_inv_exchange_rate_date` | Datum (Date) | Exchange rate date for VAT invoice |
|
||||
|
||||
### Geographic Information - Departure
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `departure_country` | `departure_country` | Zeichen (Char) | Country of departure |
|
||||
| `departure_city` | `departure_city` | Zeichen (Char) | City of departure |
|
||||
| `departure_post_code` | `departure_post_code` | Zeichen (Char) | Post code of departure |
|
||||
| `sale_depart_country` | `sale_depart_country` | Zeichen (Char) | Sale departure country |
|
||||
| `seller_depart_country_vat_number` | `seller_depart_country_vat_number` | Zeichen (Char) | Seller's VAT number in departure country |
|
||||
| `seller_depart_vat_number_country` | `seller_depart_vat_number_country` | Zeichen (Char) | Country of seller's VAT number in departure |
|
||||
|
||||
### Geographic Information - Arrival
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `arrival_country` | `arrival_country` | Zeichen (Char) | Country of arrival |
|
||||
| `arrival_city` | `arrival_city` | Zeichen (Char) | City of arrival |
|
||||
| `arrival_post_code` | `arrival_post_code` | Zeichen (Char) | Post code of arrival |
|
||||
| `arrival_address` | `arrival_address` | Text | Full arrival address |
|
||||
| `sale_arrival_country` | `sale_arrival_country` | Zeichen (Char) | Sale arrival country |
|
||||
| `seller_arrival_country_vat_number` | `seller_arrival_country_vat_number` | Zeichen (Char) | Seller's VAT number in arrival country |
|
||||
| `seller_arrival_vat_number_country` | `seller_arrival_vat_number_country` | Zeichen (Char) | Country of seller's VAT number in arrival |
|
||||
|
||||
### Geographic Information - Statistical Codes
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `statistical_code_depart` | `statistical_code_depart` | Zeichen (Char) | Statistical code for departure |
|
||||
| `statistical_code_arrival` | `statistical_code_arrival` | Zeichen (Char) | Statistical code for arrival |
|
||||
|
||||
### Buyer Information
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `buyer_name` | `buyer_name` | Zeichen (Char) | Name of the buyer |
|
||||
| `buyer_vat_number` | `buyer_vat_number` | Zeichen (Char) | VAT number of the buyer |
|
||||
| `buyer_vat_number_country` | `buyer_vat_number_country` | Zeichen (Char) | Country of buyer's VAT number |
|
||||
|
||||
### Seller Information
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `seller_sku` | `seller_sku` | Zeichen (Char) | Seller's SKU (Stock Keeping Unit) |
|
||||
| `supplier_name` | `supplier_name` | Zeichen (Char) | Name of the supplier |
|
||||
| `supplier_vat_number` | `supplier_vat_number` | Zeichen (Char) | VAT number of the supplier |
|
||||
|
||||
### Product Information
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `asin` | `asin` | Zeichen (Char) | Amazon Standard Identification Number |
|
||||
| `item_description` | `item_description` | Text | Description of the item |
|
||||
| `item_manufacture_country` | `item_manufacture_country` | Zeichen (Char) | Country of manufacture |
|
||||
| `commodity_code` | `commodity_code` | Zeichen (Char) | Commodity code for customs |
|
||||
| `commodity_code_supplementary_unit` | `commodity_code_supplementary_unit` | Zeichen (Char) | Supplementary unit for commodity code |
|
||||
|
||||
### Quantity and Weight
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `qty` | `qty` | Ganzzahl (Integer) | Quantity of items |
|
||||
| `item_qty_supplementary_unit` | `item_qty_supplementary_unit` | Ganzzahl (Integer) | Quantity in supplementary unit |
|
||||
| `item_weight` | `item_weight` | Gleitkommazahl (Float) | Weight of a single item |
|
||||
| `total_activity_weight` | `total_activity_weight` | Gleitkommazahl (Float) | Total weight of the activity |
|
||||
| `total_activity_supplementary_unit` | `total_activity_supplementary_unit` | Ganzzahl (Integer) | Total quantity in supplementary unit |
|
||||
|
||||
### Financial Fields - Price of Items
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `price_of_items_amt_vat_excl` | `price_of_items_amt_vat_excl` | Gleitkommazahl (Float) | Price of items amount excluding VAT |
|
||||
| `price_of_items_amt_vat_incl` | `price_of_items_amt_vat_incl` | Gleitkommazahl (Float) | Price of items amount including VAT |
|
||||
| `price_of_items_vat_amt` | `price_of_items_vat_amt` | Gleitkommazahl (Float) | VAT amount for price of items |
|
||||
| `price_of_items_vat_rate_percent` | `price_of_items_vat_rate_percent` | Gleitkommazahl (Float) | VAT rate percentage for price of items |
|
||||
|
||||
### Financial Fields - Promotional Price
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `promo_price_of_items_amt_vat_excl` | `promo_price_of_items_amt_vat_excl` | Gleitkommazahl (Float) | Promotional price excluding VAT |
|
||||
| `promo_price_of_items_amt_vat_incl` | `promo_price_of_items_amt_vat_incl` | Gleitkommazahl (Float) | Promotional price including VAT |
|
||||
| `promo_price_of_items_vat_amt` | `promo_price_of_items_vat_amt` | Gleitkommazahl (Float) | VAT amount for promotional price |
|
||||
|
||||
### Financial Fields - Shipping Charges
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `ship_charge_amt_vat_excl` | `ship_charge_amt_vat_excl` | Gleitkommazahl (Float) | Shipping charge amount excluding VAT |
|
||||
| `ship_charge_amt_vat_incl` | `ship_charge_amt_vat_incl` | Gleitkommazahl (Float) | Shipping charge amount including VAT |
|
||||
| `ship_charge_vat_amt` | `ship_charge_vat_amt` | Gleitkommazahl (Float) | VAT amount for shipping charges |
|
||||
| `ship_charge_vat_rate_percent` | `ship_charge_vat_rate_percent` | Gleitkommazahl (Float) | VAT rate percentage for shipping |
|
||||
|
||||
### Financial Fields - Promotional Shipping
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `promo_ship_charge_amt_vat_excl` | `promo_ship_charge_amt_vat_excl` | Gleitkommazahl (Float) | Promotional shipping excluding VAT |
|
||||
| `promo_ship_charge_amt_vat_incl` | `promo_ship_charge_amt_vat_incl` | Gleitkommazahl (Float) | Promotional shipping including VAT |
|
||||
| `promo_ship_charge_vat_amt` | `promo_ship_charge_vat_amt` | Gleitkommazahl (Float) | VAT amount for promotional shipping |
|
||||
|
||||
### Financial Fields - Gift Wrap
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `gift_wrap_amt_vat_excl` | `gift_wrap_amt_vat_excl` | Gleitkommazahl (Float) | Gift wrap amount excluding VAT |
|
||||
| `gift_wrap_amt_vat_incl` | `gift_wrap_amt_vat_incl` | Gleitkommazahl (Float) | Gift wrap amount including VAT |
|
||||
| `gift_wrap_vat_amt` | `gift_wrap_vat_amt` | Gleitkommazahl (Float) | VAT amount for gift wrap |
|
||||
| `gift_wrap_vat_rate_percent` | `gift_wrap_vat_rate_percent` | Gleitkommazahl (Float) | VAT rate percentage for gift wrap |
|
||||
|
||||
### Financial Fields - Promotional Gift Wrap
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `promo_gift_wrap_amt_vat_excl` | `promo_gift_wrap_amt_vat_excl` | Gleitkommazahl (Float) | Promotional gift wrap excluding VAT |
|
||||
| `promo_gift_wrap_amt_vat_incl` | `promo_gift_wrap_amt_vat_incl` | Gleitkommazahl (Float) | Promotional gift wrap including VAT |
|
||||
| `promo_gift_wrap_vat_amt` | `promo_gift_wrap_vat_amt` | Gleitkommazahl (Float) | VAT amount for promotional gift wrap |
|
||||
|
||||
### Financial Fields - Total Activity
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `total_price_of_items_amt_vat_excl` | `total_price_of_items_amt_vat_excl` | Gleitkommazahl (Float) | Total price of items excluding VAT |
|
||||
| `total_price_of_items_amt_vat_incl` | `total_price_of_items_amt_vat_incl` | Gleitkommazahl (Float) | Total price of items including VAT |
|
||||
| `total_price_of_items_vat_amt` | `total_price_of_items_vat_amt` | Gleitkommazahl (Float) | Total VAT amount for price of items |
|
||||
| `total_ship_charge_amt_vat_excl` | `total_ship_charge_amt_vat_excl` | Gleitkommazahl (Float) | Total shipping charge excluding VAT |
|
||||
| `total_ship_charge_amt_vat_incl` | `total_ship_charge_amt_vat_incl` | Gleitkommazahl (Float) | Total shipping charge including VAT |
|
||||
| `total_ship_charge_vat_amt` | `total_ship_charge_vat_amt` | Gleitkommazahl (Float) | Total VAT amount for shipping |
|
||||
| `total_gift_wrap_amt_vat_excl` | `total_gift_wrap_amt_vat_excl` | Gleitkommazahl (Float) | Total gift wrap excluding VAT |
|
||||
| `total_gift_wrap_amt_vat_incl` | `total_gift_wrap_amt_vat_incl` | Gleitkommazahl (Float) | Total gift wrap including VAT |
|
||||
| `total_gift_wrap_vat_amt` | `total_gift_wrap_vat_amt` | Gleitkommazahl (Float) | Total VAT amount for gift wrap |
|
||||
| `total_activity_value_amt_vat_excl` | `total_activity_value_amt_vat_excl` | Gleitkommazahl (Float) | Total activity value excluding VAT |
|
||||
| `total_activity_value_amt_vat_incl` | `total_activity_value_amt_vat_incl` | Gleitkommazahl (Float) | Total activity value including VAT |
|
||||
| `total_activity_value_vat_amt` | `total_activity_value_vat_amt` | Gleitkommazahl (Float) | Total VAT amount for activity value |
|
||||
|
||||
### Cost and Helper Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `cost_price_of_items` | `cost_price_of_items` | Gleitkommazahl (Float) | Cost price of items |
|
||||
| `helper_cogs_value` | `helper_cogs_value` | Gleitkommazahl (Float) | Helper Column: COGS Value |
|
||||
| `helper_column_1` | `helper_column_1` | Ganzzahl (Integer) | Spalte CA: BUYER_VAT_NUMBER = leer -> 1 |
|
||||
|
||||
### Tax and Compliance Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `product_tax_code` | `product_tax_code` | Zeichen (Char) | Product tax code |
|
||||
| `tax_reporting_scheme` | `tax_reporting_scheme` | Zeichen (Char) | Tax reporting scheme |
|
||||
| `taxable_jurisdiction` | `taxable_jurisdiction` | Zeichen (Char) | Taxable jurisdiction |
|
||||
| `taxable_jurisdiction_level` | `taxable_jurisdiction_level` | Zeichen (Char) | Taxable jurisdiction level |
|
||||
| `vat_calculation_imputation_country` | `vat_calculation_imputation_country` | Zeichen (Char) | Country for VAT calculation imputation |
|
||||
| `tax_collection_responsibility` | `tax_collection_responsibility` | Zeichen (Char) | Tax collection responsibility |
|
||||
|
||||
### Export Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `export_outside_eu` | `export_outside_eu` | Zeichen (Char) | Export outside EU flag |
|
||||
|
||||
### Activity Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `activity_period` | `activity_period` | Zeichen (Char) | Activity period identifier |
|
||||
| `activity_transaction_id` | `activity_transaction_id` | Zeichen (Char) | Activity transaction ID |
|
||||
|
||||
### Invoice Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `invoice_url` | `invoice_url` | Text | URL to the invoice |
|
||||
| `vat_inv_number` | `vat_inv_number` | Zeichen (Char) | VAT invoice number |
|
||||
| `vat_inv_converted_amt` | `vat_inv_converted_amt` | Gleitkommazahl (Float) | VAT invoice converted amount |
|
||||
| `vat_inv_currency_code` | `vat_inv_currency_code` | Zeichen (Char) | VAT invoice currency code |
|
||||
| `vat_inv_exchange_rate` | `vat_inv_exchange_rate` | Gleitkommazahl (Float) | VAT invoice exchange rate |
|
||||
|
||||
### Marketplace and Sales Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `marketplace` | `marketplace` | Zeichen (Char) | Marketplace where transaction occurred |
|
||||
| `sales_channel` | `sales_channel` | Zeichen (Char) | Sales channel |
|
||||
| `program_type` | `program_type` | Zeichen (Char) | Program type |
|
||||
|
||||
### Delivery and Transport Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `delivery_conditions` | `delivery_conditions` | Zeichen (Char) | Delivery conditions |
|
||||
| `transportation_mode` | `transportation_mode` | Zeichen (Char) | Transportation mode |
|
||||
|
||||
### VAT Transaction Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `transaction_seller_vat_number` | `transaction_seller_vat_number` | Zeichen (Char) | Seller's VAT number for this transaction |
|
||||
| `transaction_seller_vat_number_country` | `transaction_seller_vat_number_country` | Zeichen (Char) | Country of seller's VAT number for this transaction |
|
||||
|
||||
---
|
||||
|
||||
## Database Indexes
|
||||
|
||||
The table likely has indexes on:
|
||||
- `id` (Primary Key)
|
||||
- `create_date` / `write_date` (for audit tracking)
|
||||
- `departure_country` / `arrival_country` (for geographic filtering)
|
||||
- `transaction_type` (for transaction type filtering)
|
||||
- `transaction_event_id` (for unique transaction lookup)
|
||||
|
||||
---
|
||||
|
||||
## Usage in BI Dashboard
|
||||
|
||||
This table is queried by the following SQL scripts:
|
||||
- `Summe_je_Abgangsland.sql` - Revenue per departure country
|
||||
- `VAT_Summary.sql` - VAT summary report
|
||||
- `Alle_Rechnungen.sql` - All invoices report (combined with account.move)
|
||||
- `Rechnungsexport.sql` - Invoice export
|
||||
- `Quartal_Schweiz.sql` - Switzerland quarterly report (combined with account.move)
|
||||
- `Delete_Activity_Period.sql` - Activity period cleanup
|
||||
|
||||
**Combined Reports:**
|
||||
- `Alle_Rechnungen.sql` and `Quartal_Schweiz.sql` union Amazon transaction data with Odoo invoice data from `account.move` table
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All monetary amounts are stored as floating-point numbers
|
||||
- Date fields use PostgreSQL `DATE` or `TIMESTAMP` types
|
||||
- Text fields use PostgreSQL `TEXT` type for longer content
|
||||
- Char fields use PostgreSQL `VARCHAR` with appropriate length
|
||||
- Many2One fields reference Odoo's `res_users` table for create/write user tracking
|
||||
@@ -0,0 +1,147 @@
|
||||
# Odoo PostgreSQL Table Overview
|
||||
|
||||
This document provides an overview of the important Odoo tables used in the Brandize BI Dashboard.
|
||||
|
||||
## Main Transaction Tables
|
||||
|
||||
### `brandize_datacenter_amazon_vat_transaction`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.vat.transaction`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Auftrag (Order/Transaction) - Primary table for Amazon VAT transaction data
|
||||
|
||||
This is the main transaction table containing all VAT-related transaction data from Amazon. It serves as the foundation for all BI reports including:
|
||||
|
||||
- Revenue per departure country (`Summe_je_Abgangsland.sql`)
|
||||
- VAT summary (`VAT_Summary.sql`)
|
||||
- Invoice reports (`Alle_Rechnungen.sql`, `Rechnungsexport.sql`, `Quartal_Schweiz.sql`)
|
||||
- Activity period management (`Delete_Activity_Period.sql`)
|
||||
|
||||
---
|
||||
|
||||
### `brandize_datacenter_amazon_inventory_summary`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.inventory.summary`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Amazon Inventory Ledger Summary - Inventory ledger and stock level tracking
|
||||
|
||||
This table stores Amazon inventory ledger data, tracking inventory movements including shipments, returns, damages, and warehouse transfers. It provides visibility into inventory levels, movements, and valuation.
|
||||
|
||||
**Key Characteristics:**
|
||||
- **Type:** Basisobjekt (Base object)
|
||||
- **Has Email Thread:** No
|
||||
- **Has Email Activity:** No
|
||||
- **Has Email Blacklist:** No
|
||||
- **Transition Model:** No
|
||||
|
||||
**Related Documentation:**
|
||||
- [Full field description](brandize_datacenter_amazon_inventory_summary.md)
|
||||
|
||||
---
|
||||
|
||||
### `brandize_datacenter_amazon_inventory_detailed`
|
||||
|
||||
**Model:** `brandize.datacenter.amazon.inventory.detailed`
|
||||
**App:** `brandize_datacenter`
|
||||
**Description:** Amazon Inventory Ledger Detailed - Detailed inventory ledger entries
|
||||
|
||||
This table stores detailed inventory ledger entries for Amazon FBA. Each record represents a specific inventory event or transaction, providing a granular view of inventory movements.
|
||||
|
||||
**Key Characteristics:**
|
||||
- **Type:** Basisobjekt (Base object)
|
||||
- **Has Email Thread:** No
|
||||
- **Has Email Activity:** No
|
||||
- **Has Email Blacklist:** No
|
||||
- **Transition Model:** No
|
||||
|
||||
**Related Documentation:**
|
||||
- [Full field description](brandize_datacenter_amazon_inventory_detailed.md)
|
||||
|
||||
---
|
||||
|
||||
### `product_product`
|
||||
|
||||
**Model:** `product.product`
|
||||
**App:** `product` (with many integrations)
|
||||
**Description:** Produktvariante (Product Variant) - Odoo's core product/product variant management
|
||||
|
||||
This is Odoo's standard product variant table, storing detailed product information including variants, pricing, inventory settings, and descriptions. It serves as the central product catalog for all Odoo modules.
|
||||
|
||||
**Important:** The `default_code` field is used as the SKU (Stock Keeping Unit) throughout the system, including in Amazon integration and inventory tracking.
|
||||
|
||||
**Key Characteristics:**
|
||||
- **Type:** Basisobjekt (Base object)
|
||||
- **Has Email Thread:** Yes
|
||||
- **Has Email Activity:** Yes
|
||||
- **Has Email Blacklist:** Yes
|
||||
- **Transition Model:** No
|
||||
|
||||
**Related Documentation:**
|
||||
- [Full field description](product_product.md)
|
||||
|
||||
---
|
||||
|
||||
## Reference Tables
|
||||
|
||||
### `res_country`
|
||||
|
||||
**Model:** `res.country`
|
||||
**App:** `base`
|
||||
**Description:** Country master data table
|
||||
|
||||
Used for country code lookups and translated country names in reports.
|
||||
|
||||
**Key Fields:**
|
||||
- `id` - Primary key
|
||||
- `code` - ISO country code (e.g., 'DE', 'AT', 'CH')
|
||||
- `name` - Country name (multi-language support via JSONB)
|
||||
|
||||
**Usage:**
|
||||
- Joined in `VAT_Summary.sql` to display country names in the report
|
||||
|
||||
---
|
||||
|
||||
### `account_move`
|
||||
|
||||
**Model:** `account.move`
|
||||
**App:** `account`
|
||||
**Description:** Kundenrechnungen / Gutschriften (Customer invoices / credit notes)
|
||||
|
||||
Odoo's standard accounting module table for managing invoices and credit notes.
|
||||
|
||||
**Key Fields:**
|
||||
- `id` - Primary key
|
||||
- `name` - Invoice number
|
||||
- `move_type` - Invoice type (out_invoice, out_refund, etc.)
|
||||
- `date` - Invoice date
|
||||
- `partner_id` - Customer reference
|
||||
- `amount_total` - Total amount
|
||||
- `state` - Status (posted, draft, cancel)
|
||||
|
||||
**Usage:**
|
||||
- Queried in `Alle_Rechnungen.sql` and `Quartal_Schweiz.sql` for ODOO invoice data
|
||||
- Combined with Amazon transaction data via UNION ALL
|
||||
|
||||
---
|
||||
|
||||
## Table Structure Summary
|
||||
|
||||
| Table | Model | Description | Key Fields |
|
||||
|-------|-------|-------------|------------|
|
||||
| `brandize_datacenter_amazon_vat_transaction` | `brandize.datacenter.amazon.vat.transaction` | Amazon VAT transaction records | ID, transaction_type, departure_country, arrival_country, amount fields |
|
||||
| `brandize_datacenter_amazon_inventory_summary` | `brandize.datacenter.amazon.inventory.summary` | Amazon inventory ledger summary | ID, asin, fnsku, customer_shipments, receipts, damaged, disposed |
|
||||
| `brandize_datacenter_amazon_inventory_detailed` | `brandize.datacenter.amazon.inventory.detailed` | Amazon inventory ledger detailed | ID, asin, fnsku, event_type, quantity, reconciled_quantity, fulfillment_center |
|
||||
| `product_product` | `product.product` | Product variants | id, default_code (SKU), categ_id, list_price, cost_method |
|
||||
| `res_country` | `res.country` | Country master data | id, code, name |
|
||||
| `account_move` | `account.move` | Customer invoices / credit notes | id, name, move_type, date, partner_id, amount_total, state |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All monetary values in the database are stored in the company's currency
|
||||
- Date fields track creation (`create_date`), updates (`write_date`), and transaction-specific dates
|
||||
- The table supports multi-currency transactions with exchange rate tracking
|
||||
- Tax reporting includes multiple jurisdiction levels for VAT calculation
|
||||
- Country names in reports use multi-language support (de_DE, en_US) from `res_country.name` JSONB field
|
||||
- The BI dashboard queries Amazon transaction data (`brandize_datacenter_amazon_vat_transaction`), inventory data (`brandize_datacenter_amazon_inventory_summary`, `brandize_datacenter_amazon_inventory_detailed`), product data (`product_product`), and Odoo invoice data (`account.move`) for comprehensive reporting
|
||||
@@ -0,0 +1,458 @@
|
||||
# Table: `product_product`
|
||||
|
||||
**Model:** `product.product`
|
||||
**App:** `product` (with many integrations)
|
||||
**Description:** Produktvariante (Product Variant) - Odoo's core product/product variant management
|
||||
**Type:** Basisobjekt (Base object)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This is Odoo's standard product variant table, storing detailed product information including variants, pricing, inventory settings, and descriptions. It serves as the central product catalog for all Odoo modules including sales, purchase, inventory, accounting, and manufacturing.
|
||||
|
||||
**Important:** The `default_code` field is used as the SKU (Stock Keeping Unit) throughout the system, including in Amazon integration and inventory tracking.
|
||||
|
||||
---
|
||||
|
||||
## Fields
|
||||
|
||||
### Core Identifier Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `id` | `id` | Ganzzahl (Integer) | Primary key / Record ID |
|
||||
| `display_name` | `display_name` | Zeichen (Char) | Display name of the record |
|
||||
| `name` | `name` | Zeichen (Char) | Product name |
|
||||
|
||||
### Activity Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `create_date` | `create_date` | Datum/Zeit (Datetime) | Created on - Timestamp when record was created |
|
||||
| `create_uid` | `create_uid` | Many2One | Created by - Reference to user who created the record |
|
||||
| `write_date` | `write_date` | Datum/Zeit (Datetime) | Last Updated on - Timestamp of last modification |
|
||||
| `write_uid` | `write_uid` | Many2One | Last Updated by - Reference to user who last modified the record |
|
||||
|
||||
### Product Identification
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `default_code` | `default_code` | Zeichen (Char) | **Internal Reference / SKU** - Primary SKU identifier used throughout the system |
|
||||
| `code` | `code` | Zeichen (Char) | Reference |
|
||||
| `barcode` | `barcode` | Zeichen (Char) | Barcode / EAN / UPC |
|
||||
| `unumber` | `unumber` | Zeichen (Char) | uNumber |
|
||||
| `x_studio_char_field_5ro_1j8nka2bt` | `x_studio_char_field_5ro_1j8nka2bt` | Zeichen (Char) | Custom text field |
|
||||
|
||||
### Product Categorization
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `categ_id` | `categ_id` | Many2One | Product category |
|
||||
| `product_tag_ids` | `product_tag_ids` | Many2many | Product template tags |
|
||||
| `all_product_tag_ids` | `all_product_tag_ids` | Many2many | All product tags |
|
||||
| `account_tag_ids` | `account_tag_ids` | Many2many | Accounting tags |
|
||||
| `additional_product_tag_ids` | `additional_product_tag_ids` | Many2many | Additional product tags |
|
||||
|
||||
### Product Attributes
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `attribute_line_ids` | `attribute_line_ids` | one2many | Product attributes (lines) |
|
||||
| `combination_indices` | `combination_indices` | Zeichen (Char) | Combination indices for variant matching |
|
||||
| `has_configurable_attributes` | `has_configurable_attributes` | Boolesch (Boolean) | Is a configurable product |
|
||||
| `product_template_attribute_value_ids` | `product_template_attribute_value_ids` | Many2many | Attribute values |
|
||||
| `product_template_variant_value_ids` | `product_template_variant_value_ids` | Many2many | Variant values |
|
||||
| `valid_product_template_attribute_line_ids` | `valid_product_template_attribute_line_ids` | Many2many | Valid product attribute lines |
|
||||
|
||||
### Product Template Links
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `product_tmpl_id` | `product_tmpl_id` | Many2One | Product template |
|
||||
| `product_variant_id` | `product_variant_id` | Many2One | Product |
|
||||
| `product_variant_ids` | `product_variant_ids` | one2many | Products |
|
||||
| `product_variant_count` | `product_variant_count` | Ganzzahl (Integer) | # Product variants |
|
||||
|
||||
### Pricing
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `list_price` | `list_price` | Gleitkommazahl (Float) | Sales price (catalog price) |
|
||||
| `lst_price` | `lst_price` | Gleitkommazahl (Float) | Sales price (list price) |
|
||||
| `standard_price` | `standard_price` | Gleitkommazahl (Float) | Cost (standard price) |
|
||||
| `price_extra` | `price_extra` | Gleitkommazahl (Float) | Price extra for variant |
|
||||
| `cost_method` | `cost_method` | Auswahl (Selection) | Costing method (Standard, FIFO, Average) |
|
||||
| `avg_cost` | `avg_cost` | monetär (Monetary) | Average cost |
|
||||
| `cost_currency_id` | `cost_currency_id` | Many2One | Cost currency |
|
||||
| `company_currency_id` | `company_currency_id` | Many2One | Company currency for valuation |
|
||||
|
||||
### Company and Currency
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `company_id` | `company_id` | Many2One | Company |
|
||||
| `currency_id` | `currency_id` | Many2One | Currency |
|
||||
| `country_of_origin` | `country_of_origin` | Many2One | Country of origin |
|
||||
|
||||
### Inventory Settings
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `detailed_type` | `detailed_type` | Auswahl (Selection) | Product type (Product, Service, Kit) |
|
||||
| `free_qty` | `free_qty` | Gleitkommazahl (Float) | Available quantity (free to sell) |
|
||||
| `incoming_qty` | `incoming_qty` | Gleitkommazahl (Float) | Incoming quantity |
|
||||
| `outgoing_qty` | `outgoing_qty` | Gleitkommazahl (Float) | Outgoing quantity |
|
||||
| `location_id` | `location_id` | Many2One | Default inventory location |
|
||||
| `warehouse_id` | `warehouse_id` | Many2One | Warehouse |
|
||||
| `property_stock_inventory` | `property_stock_inventory` | Many2One | Inventory location |
|
||||
| `property_stock_production` | `property_stock_production` | Many2One | Production location |
|
||||
| `property_account_creditor_price_difference` | `property_account_creditor_price_difference` | Many2One | Price difference account |
|
||||
| `property_account_expense_id` | `property_account_expense_id` | Many2One | Expense account |
|
||||
| `property_account_income_id` | `property_account_income_id` | Many2One | Income account |
|
||||
|
||||
### Quantity and Stock
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `bom_count` | `bom_count` | Ganzzahl (Integer) | Number of bills of material |
|
||||
| `bom_ids` | `bom_ids` | one2many | Bills of material |
|
||||
| `bom_line_ids` | `bom_line_ids` | one2many | Bill of material lines |
|
||||
| `variant_bom_ids` | `variant_bom_ids` | one2many | Bill of material product variants |
|
||||
| `mrp_product_qty` | `mrp_product_qty` | Gleitkommazahl (Float) | Manufactured quantity |
|
||||
| `used_in_bom_count` | `used_in_bom_count` | Ganzzahl (Integer) | # Bills of material where used |
|
||||
| `qty_available` | `qty_available` | Gleitkommazahl (Float) | Quantity on hand |
|
||||
| `virtual_available` | `virtual_available` | Gleitkommazahl (Float) | Forecasted quantity |
|
||||
| `nbr_moves_in` | `nbr_moves_in` | Ganzzahl (Integer) | Number of incoming moves |
|
||||
| `nbr_moves_out` | `nbr_moves_out` | Ganzzahl (Integer) | Number of outgoing moves |
|
||||
| `stock_move_ids` | `stock_move_ids` | one2many | Stock moves |
|
||||
| `stock_quant_ids` | `stock_quant_ids` | one2many | Stock quants |
|
||||
| `stock_valuation_layer_ids` | `stock_valuation_layer_ids` | one2many | Stock valuation layers |
|
||||
|
||||
### Country and Compliance
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `hs_code` | `hs_code` | Zeichen (Char) | HS Code (Harmonized System code for customs) |
|
||||
| `fiscal_country_codes` | `fiscal_country_codes` | Zeichen (Char) | Fiscal country codes |
|
||||
| `origin_area` | `origin_area` | Zeichen (Char) | Origin Area |
|
||||
| `valid_ean` | `valid_ean` | Boolesch (Boolean) | Barcode is a valid EAN |
|
||||
|
||||
### Expiration and Quality
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `alert_time` | `alert_time` | Ganzzahl (Integer) | Alert time (expiration warning) |
|
||||
| `expiration_time` | `expiration_time` | Ganzzahl (Integer) | Expiration time |
|
||||
| `use_expiration_date` | `use_expiration_date` | Boolesch (Boolean) | Use expiration date |
|
||||
| `use_time` | `use_time` | Ganzzahl (Integer) | Minimum shelf life |
|
||||
| `removal_time` | `removal_time` | Ganzzahl (Integer) | Removal date |
|
||||
| `product_expiry` | (module) | - | Product expiration tracking |
|
||||
| `quality_control_point_qty` | `quality_control_point_qty` | Ganzzahl (Integer) | Number of quality control points |
|
||||
| `quality_fail_qty` | `quality_fail_qty` | Ganzzahl (Integer) | Estimated failed quality controls |
|
||||
| `quality_pass_qty` | `quality_pass_qty` | Ganzzahl (Integer) | Estimated passed quality controls |
|
||||
|
||||
### Descriptions
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `description` | `description` | HTML | General description |
|
||||
| `description_picking` | `description_picking` | Text | Description for picking operations |
|
||||
| `description_pickingin` | `description_pickingin` | Text | Description for receipt operations |
|
||||
| `description_pickingout` | `description_pickingout` | Text | Description for delivery operations |
|
||||
| `description_purchase` | `description_purchase` | Text | Purchase description |
|
||||
| `description_sale` | `description_sale` | Text | Sales description |
|
||||
|
||||
### Status and Flags
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `active` | `active` | Boolesch (Boolean) | Is active |
|
||||
| `is_product_variant` | `is_product_variant` | Boolesch (Boolean) | Is a product variant |
|
||||
| `is_kits` | `is_kits` | Boolesch (Boolean) | Is a kit/assembly |
|
||||
| `landed_cost_ok` | `landed_cost_ok` | Boolesch (Boolean) | Is a landed cost product |
|
||||
| `sale_ok` | `sale_ok` | Boolesch (Boolean) | Can be sold |
|
||||
| `purchase_ok` | `purchase_ok` | Boolesch (Boolean) | Can be purchased |
|
||||
| `service_to_purchase` | `service_to_purchase` | Boolesch (Boolean) | Subcontractor service |
|
||||
| `exporto_sync` | `exporto_sync` | Boolesch (Boolean) | Exporto sync flag |
|
||||
| `exporto_synced_product` | `exporto_synced_product` | Boolesch (Boolean) | Exporto synced product flag |
|
||||
| `of_is_shopify_linked` | `of_is_shopify_linked` | Boolesch (Boolean) | Is Shopify product? |
|
||||
| `visible_expense_policy` | `visible_expense_policy` | Boolesch (Boolean) | Expense policy visible |
|
||||
|
||||
### Image Fields
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `image_1920` | `image_1920` | binär (Binary) | Product image (1920px) |
|
||||
| `image_1024` | `image_1024` | binär (Binary) | Product image (1024px) |
|
||||
| `image_512` | `image_512` | binär (Binary) | Product image (512px) |
|
||||
| `image_256` | `image_256` | binär (Binary) | Product image (256px) |
|
||||
| `image_128` | `image_128` | binär (Binary) | Product image (128px) |
|
||||
| `image_variant_1920` | `image_variant_1920` | binär (Binary) | Variant image (1920px) |
|
||||
| `image_variant_1024` | `image_variant_1024` | binär (Binary) | Variant image (1024px) |
|
||||
| `image_variant_512` | `image_variant_512` | binär (Binary) | Variant image (512px) |
|
||||
| `image_variant_256` | `image_variant_256` | binär (Binary) | Variant image (256px) |
|
||||
| `image_variant_128` | `image_variant_128` | binär (Binary) | Variant image (128px) |
|
||||
| `can_image_1024_be_zoomed` | `can_image_1024_be_zoomed` | Boolesch (Boolean) | Can zoom 1024 image |
|
||||
| `can_image_variant_1024_be_zoomed` | `can_image_variant_1024_be_zoomed` | Boolesch (Boolean) | Can zoom variant 1024 image |
|
||||
|
||||
### Color
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `color` | `color` | Ganzzahl (Integer) | Color picker value |
|
||||
|
||||
### INCI (Cosmetics)
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `inci_ids` | `inci_ids` | Many2many | INCI (International Nomenclature of Cosmetic Ingredients) |
|
||||
| `x_studio_inci` | `x_studio_inci` | Zeichen (Char) | INCI (custom field) |
|
||||
|
||||
### Product Packaging
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `packaging_ids` | `packaging_ids` | one2many | Product packaging |
|
||||
| `uom_id` | `uom_id` | Many2One | Unit of measure |
|
||||
| `uom_name` | `uom_name` | Zeichen (Char) | Unit of measure name |
|
||||
| `uom_po_id` | `uom_po_id` | Many2One | Purchase unit of measure |
|
||||
|
||||
### Product Properties
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `product_properties` | `product_properties` | properties | Properties |
|
||||
| `lot_properties_definition` | `lot_properties_definition` | properties_definition | Lot properties definition |
|
||||
|
||||
### Pricing and Accounting
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `pricelist_item_count` | `pricelist_item_count` | Ganzzahl (Integer) | Number of price rules |
|
||||
| `offer_count` | `offer_count` | Ganzzahl (Integer) | Number of offers |
|
||||
| `price_extra` | `price_extra` | Gleitkommazahl (Float) | Price extra for variant |
|
||||
|
||||
### Product Catalog
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `product_catalog_product_is_in_sale_order` | `product_catalog_product_is_in_sale_order` | Boolesch (Boolean) | Product catalog product is in sale order |
|
||||
| `product_document_count` | `product_document_count` | Ganzzahl (Integer) | Number of documents |
|
||||
| `product_document_ids` | `product_document_ids` | one2many | Documents |
|
||||
|
||||
### Routing and Route
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `route_ids` | `route_ids` | Many2many | Routes |
|
||||
| `route_from_categ_ids` | `route_from_categ_ids` | Many2many | Route category |
|
||||
|
||||
### Reordering Rules
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `orderpoint_ids` | `orderpoint_ids` | one2many | Reordering rules |
|
||||
| `nbr_reordering_rules` | `nbr_reordering_rules` | Ganzzahl (Integer) | Number of reordering rules |
|
||||
| `reordering_min_qty` | `reordering_min_qty` | Gleitkommazahl (Float) | Minimum reordering quantity |
|
||||
| `reordering_max_qty` | `reordering_max_qty` | Gleitkommazahl (Float) | Maximum reordering quantity |
|
||||
|
||||
### Supplier and Purchase
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `seller_ids` | `seller_ids` | one2many | Suppliers |
|
||||
| `variant_seller_ids` | `variant_seller_ids` | one2many | Variant suppliers |
|
||||
| `purchase_line_warn` | `purchase_line_warn` | Auswahl (Selection) | Purchase line warning |
|
||||
| `purchase_line_warn_msg` | `purchase_line_warn_msg` | Text | Purchase line warning message |
|
||||
| `purchase_method` | `purchase_method` | Auswahl (Selection) | Control policy |
|
||||
| `purchase_order_line_ids` | `purchase_order_line_ids` | one2many | Purchase order lines |
|
||||
| `purchased_product_qty` | `purchased_product_qty` | Gleitkommazahl (Float) | Purchased quantity |
|
||||
|
||||
### Sales
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `sale_line_warn` | `sale_line_warn` | Auswahl (Selection) | Sale order line warning |
|
||||
| `sale_line_warn_msg` | `sale_line_warn_msg` | Text | Sale order line warning message |
|
||||
| `sale_delay` | `sale_delay` | Ganzzahl (Integer) | Customer lead time |
|
||||
| `sales_count` | `sales_count` | Gleitkommazahl (Float) | Sold quantity |
|
||||
| `optional_product_ids` | `optional_product_ids` | Many2many | Optional products |
|
||||
|
||||
### Inventory Valuation
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `quantity_svl` | `quantity_svl` | Gleitkommazahl (Float) | BBE quantity (Batch/Batch Entry) |
|
||||
| `value_svl` | `value_svl` | Gleitkommazahl (Float) | BBE value |
|
||||
| `total_value` | `total_value` | monetär (Monetary) | Total value |
|
||||
| `valuation` | `valuation` | Auswahl (Selection) | Stock valuation |
|
||||
|
||||
### Weight and Volume
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `weight` | `weight` | Gleitkommazahl (Float) | Weight |
|
||||
| `weight_uom_name` | `weight_uom_name` | Zeichen (Char) | Weight unit of measure name |
|
||||
| `volume` | `volume` | Gleitkommazahl (Float) | Volume |
|
||||
| `volume_uom_name` | `volume_uom_name` | Zeichen (Char) | Volume unit of measure name |
|
||||
| `specific_weight` | `specific_weight` | Gleitkommazahl (Float) | Specific Weight (g/ml) |
|
||||
|
||||
### Tax
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `taxes_id` | `taxes_id` | Many2many | Customer taxes |
|
||||
| `supplier_taxes_id` | `supplier_taxes_id` | Many2many | Supplier taxes |
|
||||
| `tax_string` | `tax_string` | Zeichen (Char) | Tax string |
|
||||
|
||||
### Tracking
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `tracking` | `tracking` | Auswahl (Selection) | Tracking |
|
||||
|
||||
### Type
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `type` | `type` | Auswahl (Selection) | Type |
|
||||
| `service_type` | `service_type` | Auswahl (Selection) | Track service |
|
||||
|
||||
### Priority and Display
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `priority` | `priority` | Auswahl (Selection) | Favorite |
|
||||
| `sequence` | `sequence` | Ganzzahl (Integer) | Sequence |
|
||||
| `product_tooltip` | `product_tooltip` | Zeichen (Char) | Product tooltip |
|
||||
|
||||
### Shopify Integration
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `of_shopify_bind_ids` | `of_shopify_bind_ids` | one2many | Shopify bindings |
|
||||
| `shopify_variant_id` | `shopify_variant_id` | Zeichen (Char) | Shopify variant ID |
|
||||
|
||||
### Stock and Moves
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `schedule_count` | `schedule_count` | Ganzzahl (Integer) | Schedules |
|
||||
| `split_method_landed_cost` | `split_method_landed_cost` | Auswahl (Selection) | Standard split method for landed cost |
|
||||
|
||||
### Putaway Rules
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `putaway_rule_ids` | `putaway_rule_ids` | one2many | Putaway rules |
|
||||
| `storage_category_capacity_ids` | `storage_category_capacity_ids` | one2many | Storage category capacity |
|
||||
|
||||
### Website
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `website_message_ids` | `website_message_ids` | one2many | Website messages |
|
||||
|
||||
### Responsible
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `responsible_id` | `responsible_id` | Many2One | Responsible |
|
||||
|
||||
### Partner
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `partner_ref` | `partner_ref` | Zeichen (Char) | Customer reference |
|
||||
|
||||
### Activity and Messaging
|
||||
|
||||
| Field Name | Database Column | Type | Description |
|
||||
|------------|-----------------|------|-------------|
|
||||
| `activity_ids` | `activity_ids` | one2many | Activities |
|
||||
| `activity_type_id` | `activity_type_id` | Many2One | Next activity type |
|
||||
| `activity_state` | `activity_state` | Auswahl (Selection) | Activity status |
|
||||
| `activity_user_id` | `activity_user_id` | Many2One | Responsible user |
|
||||
| `activity_date_deadline` | `activity_date_deadline` | Datum (Date) | Next activity deadline |
|
||||
| `activity_summary` | `activity_summary` | Zeichen (Char) | Next activity summary |
|
||||
| `activity_exception_icon` | `activity_exception_icon` | Zeichen (Char) | Exception icon |
|
||||
| `activity_exception_decoration` | `activity_exception_decoration` | Auswahl (Selection) | Exception decoration |
|
||||
| `activity_type_icon` | `activity_type_icon` | Zeichen (Char) | Activity type icon |
|
||||
| `my_activity_date_deadline` | `my_activity_date_deadline` | Datum (Date) | Deadline for my activities |
|
||||
| `message_ids` | `message_ids` | one2many | Messages |
|
||||
| `message_follower_ids` | `message_follower_ids` | one2many | Followers |
|
||||
| `message_has_error` | `message_has_error` | Boolesch (Boolean) | Message delivery error |
|
||||
| `message_has_error_counter` | `message_has_error_counter` | Ganzzahl (Integer) | Number of errors |
|
||||
| `message_has_sms_error` | `message_has_sms_error` | Boolesch (Boolean) | SMS delivery error |
|
||||
| `message_attachment_count` | `message_attachment_count` | Ganzzahl (Integer) | Number of attachments |
|
||||
| `message_is_follower` | `message_is_follower` | Boolesch (Boolean) | Is follower |
|
||||
| `message_needaction` | `message_needaction` | Boolesch (Boolean) | Action needed |
|
||||
| `message_needaction_counter` | `message_needaction_counter` | Ganzzahl (Integer) | Number of actions |
|
||||
| `message_partner_ids` | `message_partner_ids` | Many2many | Followers (partners) |
|
||||
|
||||
---
|
||||
|
||||
## Related Odoo Models
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `product.template` | Product template (parent of product.product) |
|
||||
| `product.category` | Product category |
|
||||
| `product.tag` | Product tag |
|
||||
| `uom.uom` | Unit of measure |
|
||||
| `stock.location` | Stock location |
|
||||
| `res.country` | Country |
|
||||
| `account.account` | Accounting account |
|
||||
| `mrp.bom` | Bill of material |
|
||||
| `purchase.order.line` | Purchase order line |
|
||||
| `sale.order.line` | Sale order line |
|
||||
|
||||
---
|
||||
|
||||
## Database Indexes
|
||||
|
||||
The table likely has indexes on:
|
||||
- `id` (Primary Key)
|
||||
- `create_date` / `write_date` (for audit tracking)
|
||||
- `default_code` (for SKU lookups - **critical index**)
|
||||
- `barcode` (for barcode scanning)
|
||||
- `categ_id` (for category filtering)
|
||||
- `company_id` (for multi-company support)
|
||||
- `active` (for filtering inactive products)
|
||||
- `detailed_type` (for product type filtering)
|
||||
|
||||
---
|
||||
|
||||
## Usage in BI Dashboard
|
||||
|
||||
This table can be used for:
|
||||
|
||||
- **Product catalog reporting** - Full product information including descriptions, pricing, categories
|
||||
- **SKU tracking** - Using `default_code` as the primary SKU identifier
|
||||
- **Inventory valuation** - Using `avg_cost`, `standard_price`, `cost_method`, and currency fields
|
||||
- **Product variant analysis** - Tracking different variants of the same product template
|
||||
- **Barcode integration** - Using `barcode` field for scanning and tracking
|
||||
- **Country of origin reporting** - Using `country_of_origin` and `hs_code` for customs
|
||||
- **Expiration tracking** - Using `alert_time`, `expiration_time`, and `use_expiration_date` for perishable goods
|
||||
- **Kit/assembly tracking** - Using `is_kits` flag for bundle products
|
||||
- **Shopify integration** - Using `of_is_shopify_linked` and `shopify_variant_id` fields
|
||||
- **Inventory moves** - Using `nbr_moves_in`, `nbr_moves_out`, and `stock_move_ids` for movement analysis
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `default_code` is the primary SKU identifier used throughout Odoo and in integrations (including Amazon)
|
||||
- This table represents individual product variants; product templates (`product.template`) define the base product
|
||||
- Multiple integrations use this table: `account`, `sale`, `purchase`, `stock`, `mrp`, `quality_control`, `sale_amazon`, etc.
|
||||
- The `detailed_type` field distinguishes between Product (physical), Service, and Kit/Assembly
|
||||
- `free_qty` represents available quantity excluding reserved stock
|
||||
- `virtual_available` is the forecasted quantity (available - incoming + outgoing)
|
||||
- Product images are stored in multiple sizes for different display contexts
|
||||
- The table supports complex costing methods: Standard, FIFO, and Average (via `cost_method`)
|
||||
- INCI fields are for cosmetics industry compliance
|
||||
- Exporto integration fields track synchronization status with external systems
|
||||
- Shopify integration fields track product bindings to Shopify
|
||||
- Stock valuation is tracked via `stock_valuation_layer_ids` and `quantity_svl`/`value_svl`
|
||||
- Tax configuration uses both customer (`taxes_id`) and supplier (`supplier_taxes_id`) tax fields
|
||||
+44
-7
@@ -285,6 +285,7 @@ select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px
|
||||
}
|
||||
.panel-head h2 { margin: 0; font-size: 16px; letter-spacing: -.01em; }
|
||||
.muted { color: var(--muted); font-size: 13px; }
|
||||
.panel-note { margin: 0 0 14px; }
|
||||
|
||||
.legend { display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
.legend-item { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); }
|
||||
@@ -319,20 +320,56 @@ select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px
|
||||
}
|
||||
|
||||
/* ---------- Table ---------- */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
/* Bounded height so the table body scrolls (both axes) while the header row
|
||||
stays put. */
|
||||
.table-wrap { overflow: auto; max-height: 70vh; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
thead th {
|
||||
.table-wrap thead th {
|
||||
position: sticky; top: 0; z-index: 2;
|
||||
background: var(--surface);
|
||||
text-align: left; font-size: 11.5px; letter-spacing: .05em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
padding: 10px 12px; border-bottom: 1px solid var(--border);
|
||||
padding: 10px 12px; box-shadow: inset 0 -1px 0 var(--border);
|
||||
font-weight: 600;
|
||||
}
|
||||
tbody td { padding: 11px 12px; border-bottom: 1px solid var(--surface-2); }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover td { background: var(--surface-2); }
|
||||
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.table-wrap tbody td { padding: 11px 12px; border-bottom: 1px solid var(--surface-2); }
|
||||
.table-wrap tbody tr:last-child td { border-bottom: none; }
|
||||
.table-wrap tbody tr:hover td { background: var(--surface-2); }
|
||||
.table-wrap th.num, .table-wrap td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
tr.total td { font-weight: 700; background: var(--accent-soft); }
|
||||
tr.total:hover td { background: var(--accent-soft); }
|
||||
tr.summary td { font-weight: 700; background: var(--accent-soft); }
|
||||
tr.summary:hover td { background: var(--accent-soft); }
|
||||
|
||||
/* ---------- PivotTable.js experiment ---------- */
|
||||
#pivot { overflow-x: auto; }
|
||||
.panel-head-actions { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
/* ---------- Per-dashboard table filters ---------- */
|
||||
.table-filters {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.table-filters input {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
min-width: 150px;
|
||||
transition: border-color .15s, box-shadow .15s, background .15s;
|
||||
}
|
||||
.table-filters input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-block; padding: 2px 9px; border-radius: 999px;
|
||||
|
||||
Reference in new issue
Block a user