From 9db080faeaff80f27ca113ec639509a2de3146e7 Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 12 Oct 2026 10:09:44 -0300 Subject: [PATCH] not bad --- AGENTS.md | 133 ++++++++++ README.md | 67 +++++- app.js | 522 +++++++++++++++++++++++++++++++++++++--- index.html | 212 +++++++++++++++- sql/Alle_Rechnungen.sql | 415 ++++++++++++++++++++++++++++++++ sql/Quartal_Schweiz.sql | 428 ++++++++++++++++++++++++++++++++ sql/Rechnungsexport.sql | 292 ++++++++++++++++++++++ styles.css | 50 +++- 8 files changed, 2064 insertions(+), 55 deletions(-) create mode 100644 AGENTS.md create mode 100644 sql/Alle_Rechnungen.sql create mode 100644 sql/Quartal_Schweiz.sql create mode 100644 sql/Rechnungsexport.sql diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2e6b9f5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,133 @@ +# 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 `
` 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/.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 ` + + diff --git a/sql/Alle_Rechnungen.sql b/sql/Alle_Rechnungen.sql new file mode 100644 index 0000000..17d8d8b --- /dev/null +++ b/sql/Alle_Rechnungen.sql @@ -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"; diff --git a/sql/Quartal_Schweiz.sql b/sql/Quartal_Schweiz.sql new file mode 100644 index 0000000..ac1d783 --- /dev/null +++ b/sql/Quartal_Schweiz.sql @@ -0,0 +1,428 @@ +-- ============================================================================= +-- 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' +-- +-- --------------------------------------------------------------------------- +-- 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 +), +-- Die drei activity_period-Werte (Format 'YYYY-MMM', deutsche Kuerzel) fuer die +-- Amazon-Tabelle. +qmonths AS ( + SELECT + q.yr || '-' || CASE ((q.qn - 1) * 3 + m.m) + WHEN 1 THEN 'JAN' WHEN 2 THEN 'FEB' WHEN 3 THEN 'MÄR' + WHEN 4 THEN 'APR' WHEN 5 THEN 'MAI' WHEN 6 THEN 'JUN' + WHEN 7 THEN 'JUL' WHEN 8 THEN 'AUG' WHEN 9 THEN 'SEP' + WHEN 10 THEN 'OKT' WHEN 11 THEN 'NOV' WHEN 12 THEN 'DEZ' + END AS activity_period + FROM quarters q + CROSS JOIN generate_series(1, 3) AS m(m) +), + +-- =========================================================================== +-- 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 a.activity_period IN (SELECT activity_period FROM qmonths) + AND a.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"; diff --git a/sql/Rechnungsexport.sql b/sql/Rechnungsexport.sql new file mode 100644 index 0000000..9c644a3 --- /dev/null +++ b/sql/Rechnungsexport.sql @@ -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; diff --git a/styles.css b/styles.css index 147263e..1cd308e 100644 --- a/styles.css +++ b/styles.css @@ -319,20 +319,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;