feat(123): receipts export xlsx VAT breakdown

- AccountingController::export(): new headers (Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT), removed Data sprzedazy/Konfiguracja/Nr zamowienia/Nr referencyjny
- buildVatBreakdown() helper groups items_json by vat rate, emits one XLSX row per (receipt x rate); legacy receipts (no `vat` in snapshot) fallback to net=brutto/1.23
- ReceiptService::buildItemsSnapshot(): writes `vat` per item from order_items.tax_rate (fallback 23.0); shipping cost item gets vat=23.0
- RECEIPT-NET-FIX deferred (.paul/codebase/todo.md): ReceiptService::issue() still saves total_net=total_gross

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:06:53 +02:00
parent a4ed4531dc
commit 0227f2d072
12 changed files with 491 additions and 17 deletions

View File

@@ -0,0 +1,175 @@
---
phase: 123-receipts-export-vat-breakdown
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/Modules/Accounting/AccountingController.php
- src/Modules/Accounting/ReceiptService.php
autonomous: true
delegation: off
---
<objective>
## Goal
Zmodyfikować eksport paragonów do XLSX (`/accounting`, akcja `AccountingController::export`) tak, aby:
- usunąć kolumny: `Data sprzedaży`, `Konfiguracja`, `Nr zamówienia`, `Nr referencyjny`,
- dodać kolumny: `Kwota netto`, `Stawka VAT`, `Kwota VAT`,
- emitować osobny wiersz na każdą stawkę VAT występującą w paragonie (multi-rate breakdown).
## Purpose
Operator/księgowy potrzebuje arkusza, który da się wprost zaczytać do księgowości — z rozbiciem netto/VAT per stawka. Obecny eksport zawiera dane zbędne dla księgowości (data sprzedaży, konfiguracja, referencje) a nie ma podstawowych pól VAT.
## Output
- Zmodyfikowany `AccountingController::export()` — nagłówki: `Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT`.
- Rozszerzony `ReceiptService::buildItemsSnapshot()``items_json` przechowuje `vat` (procent) per pozycja dla nowych paragonów.
- Helper grupujący `items_json` po stawce VAT (z fallbackiem 23% dla legacy paragonów bez `vat`).
- Plik `paragony_YYYY-MM-DD.xlsx` z jednym wierszem per (paragon × stawka VAT).
</objective>
<context>
<clarifications>
- **Liczenie VAT** — Jak wyliczać 'Stawkę VAT' i 'Kwotę VAT'?
→ Odpowiedź: Jeżeli paragon ma produkty z kilkoma stawkami, wszystkie powinny być wypisane (np. 23%, 8%) — jako osobny wiersz w XLSX per stawka.
- **Źródło netto** — Czy 'Kwota netto' z `receipts.total_net` czy wyliczana?
→ Odpowiedź: Z `receipts.total_net` (snapshot z momentu wystawienia). Per stawka — wyliczane z pozycji items_json grupowanych po VAT.
- **Format kwot** — Jak prezentować przy multi-rate?
→ Odpowiedź: Osobny wiersz per stawka (ten sam Numer, różne stawki/kwoty) — pozwala sumować w Excelu bez rozbijania komórek.
- **Legacy data** — Brak `vat` w `items_json` historycznych paragonów.
→ Odpowiedź: Dodać `vat` do snapshotu w `buildItemsSnapshot()` (nowe paragony). Dla starych paragonów (snapshot bez `vat`) — jeden wiersz w eksporcie ze stawką 23%, `Kwota netto = total_net`, `Kwota VAT = total_gross total_net`.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
## Source Files
@src/Modules/Accounting/AccountingController.php
@src/Modules/Accounting/ReceiptService.php
@src/Modules/Accounting/ReceiptRepository.php
</context>
<acceptance_criteria>
## AC-1: Nagłówki XLSX zgodne z wymaganiem
```gherkin
Given operator otwiera /accounting i klika "Eksportuj XLSX"
When pobiera plik paragony_YYYY-MM-DD.xlsx
Then arkusz "Paragony" ma dokładnie 6 kolumn w kolejności: Numer, Data wystawienia, Kwota brutto, Kwota netto, Stawka VAT, Kwota VAT
And nie ma kolumn: Data sprzedazy, Konfiguracja, Nr zamowienia, Nr referencyjny
```
## AC-2: Multi-rate breakdown dla nowych paragonów
```gherkin
Given paragon (po Phase 123) zawiera 2 pozycje: A z VAT 23% (netto 100, brutto 123) i B z VAT 8% (netto 50, brutto 54)
When operator eksportuje XLSX
Then arkusz zawiera 2 wiersze dla tego paragonu (ten sam Numer, ta sama Data wystawienia, ta sama Kwota brutto = 177)
And wiersz #1: Stawka VAT = "23%", Kwota netto = 100.00, Kwota VAT = 23.00
And wiersz #2: Stawka VAT = "8%", Kwota netto = 50.00, Kwota VAT = 4.00
And suma kolumny Kwota netto dla tego paragonu = total_net z bazy
```
## AC-3: Legacy paragony (bez `vat` w items_json) — fallback 23%
```gherkin
Given paragon wystawiony przed Phase 123 (items_json bez klucza `vat`), z total_net=200, total_gross=246
When operator eksportuje XLSX
Then dla tego paragonu jest dokładnie 1 wiersz
And Stawka VAT = "23%", Kwota netto = 200.00, Kwota VAT = 46.00 (= total_gross total_net)
```
## AC-4: Snapshot rozszerzony o VAT (nowe paragony)
```gherkin
Given operator wystawia nowy paragon z zamówienia (po Phase 123)
When `ReceiptService::issue()` zapisuje rekord w tabeli receipts
Then każda pozycja w items_json ma klucz `vat` (procent jako float, np. 23.0 lub 8.0)
And pozycja "Koszt wysylki" (gdy delivery_price > 0) ma `vat` = 23.0 (domyślny)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Rozszerz items_json snapshot o stawkę VAT per pozycja</name>
<files>src/Modules/Accounting/ReceiptService.php</files>
<action>
W `buildItemsSnapshot()` (linie 255-291):
- Dla każdego item dodaj klucz `vat` w snapshocie. Źródło: `$item['vat']` jeżeli ustawiony, fallback `(float) ($item['original_vat'] ?? 23.0)`.
- Sprawdź jakie pola item-y mają w obecnej formie (controller/service które wywołują issue() — prawdopodobnie z order_items.vat). Jeżeli brak — dodaj fallback 23.0.
- Pozycja "Koszt wysylki" (linie 277-284) — dodaj `'vat' => 23.0` (jako domyślną dla wysyłki).
Zachowaj BC: stary snapshot bez `vat` musi nadal działać przy odczycie (export i widok print/preview).
Avoid: zmiany struktury kwot (price/total) — tylko dodanie pola `vat`. Brak migracji DB (items_json jest JSON-em).
</action>
<verify>Wystaw nowy paragon testowy w UI; sprawdź w bazie: `SELECT items_json FROM receipts ORDER BY id DESC LIMIT 1;` — każda pozycja ma klucz `vat`.</verify>
<done>AC-4 satisfied: nowe paragony zawierają `vat` w items_json.</done>
</task>
<task type="auto">
<name>Task 2: Przepisz AccountingController::export() — nowe nagłówki + multi-rate breakdown</name>
<files>src/Modules/Accounting/AccountingController.php</files>
<action>
W metodzie `export()` (linie 109-168):
1. Zmień `$headers` na: `['Numer', 'Data wystawienia', 'Kwota brutto', 'Kwota netto', 'Stawka VAT', 'Kwota VAT']` (6 kolumn).
2. Zastąp pętlę `foreach ($rows as $row)` logiką per-rate breakdown:
- Dla każdego paragonu zdekoduj `items_json`.
- Pogrupuj pozycje po `vat` (klucz: float jako string, np. "23.0").
- Dla każdej grupy oblicz `Kwota netto` (suma `total / (1 + vat/100)`, zaokrąglona do 2 miejsc) i `Kwota VAT` (`total - netto`).
- Wypisz jeden wiersz XLSX na grupę: Numer + Data wystawienia + Kwota brutto (total z paragonu, ten sam dla wszystkich wierszy) + Kwota netto (per grupa) + Stawka VAT (np. "23%") + Kwota VAT (per grupa).
3. **Fallback dla legacy paragonów** (gdy żadna pozycja items_json nie ma klucza `vat` lub items_json puste):
- Emituj 1 wiersz: Stawka VAT = "23%", Kwota netto = (float) $row['total_net'], Kwota VAT = (float) $row['total_gross'] - (float) $row['total_net'].
4. Zaktualizuj `foreach (range(1, 7) as $col)``range(1, 6)`.
5. Zachowaj formatowanie issueDateRaw (substr 0,16) dla "Data wystawienia".
Zaokrąglenia: użyj `round(..., 2)` dla netto i VAT. Suma netto+VAT per grupa może różnić się o 0.01 od `total` grupy z powodu zaokrąglenia — to akceptowalne (księgowość standardowo to toleruje).
Avoid: dodawanie nowych pól do `ReceiptRepository::exportData()` — wszystko liczymy z istniejących kolumn (`items_json`, `total_net`, `total_gross`).
</action>
<verify>Wyeksportuj XLSX zawierający (a) nowy paragon multi-rate, (b) legacy paragon bez `vat`. Otwórz w Excelu — sprawdź kolumny i wiersze zgodnie z AC-1, AC-2, AC-3.</verify>
<done>AC-1 + AC-2 + AC-3 satisfied: eksport ma nowe kolumny i poprawne rozbicie per stawka.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Struktura tabeli `receipts` (żadnej migracji DDL).
- `ReceiptRepository::exportData()` / `findByIds()` — kontrakt zapytania bez zmian (nowe pola obliczamy w controllerze z istniejących).
- Widoki paragonu (`receipts/print.php`, `receipts/preview.php`) — VAT w items_json jest dodatkowy, stare widoki nie wymagają zmian.
- Liczenie `total_gross` w `buildItemsSnapshot` — nie ruszać.
- `InvoiceService` (faktury VAT) — out of scope, ma własny multi-rate breakdown w PDF.
## SCOPE LIMITS
- Bez wstecznej migracji legacy items_json (nie aktualizujemy historycznych paragonów — fallback w eksporcie wystarczy).
- Bez UI toggle "tryb eksportu legacy/multi-rate" — tylko jedna nowa wersja eksportu.
- Bez zmian w eksporcie/widoku faktur (`/settings/accounting/invoices/issued`).
- Bez zmian w przyciskach selekcji/filtrach na `/accounting`.
</boundaries>
<verification>
- [ ] Wystawienie nowego paragonu produkuje items_json z `vat` per pozycja
- [ ] Eksport "wszystkie paragony" z mieszanym datasetem (nowe + legacy) generuje poprawny XLSX
- [ ] Eksport "wybrane paragony" działa identycznie (Task 2 dotyczy obu ścieżek `$rows`)
- [ ] Liczba kolumn = 6, brak kolumn z usuniętej listy
- [ ] Paragon z 1 stawką = 1 wiersz; paragon z 2 stawkami = 2 wiersze; legacy = 1 wiersz
</verification>
<success_criteria>
- Wszystkie tasks ukończone
- AC-1, AC-2, AC-3, AC-4 zweryfikowane manualnie na XAMPP
- Brak błędów PHP w log (`storage/logs/app.log`) podczas eksportu
- Plik XLSX otwiera się poprawnie w Excelu / LibreOffice
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/123-receipts-export-vat-breakdown/123-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,137 @@
---
phase: 123-receipts-export-vat-breakdown
plan: 01
subsystem: accounting
tags: [receipts, xlsx-export, vat, phpspreadsheet]
requires:
- phase: 12-accounting-list
provides: AccountingController::export() z PhpSpreadsheet/Xlsx
- phase: 70-receipt-shipping-cost
provides: ReceiptService::buildItemsSnapshot() snapshot pattern z delivery
provides:
- items_json snapshot z polem `vat` per pozycja (nowe paragony)
- XLSX eksport paragonow z rozbiciem per stawka VAT
- helper buildVatBreakdown() + formatVatRate() w AccountingController
affects: [accounting/invoices, statistics, ksiegowosc-eksport]
tech-stack:
added: []
patterns:
- "Per-rate VAT breakdown w eksporcie XLSX (jeden wiersz per (paragon x stawka))"
- "Legacy fallback: gdy snapshot nie ma `vat`, licz netto z brutto/(1+0.23)"
key-files:
created: []
modified:
- src/Modules/Accounting/ReceiptService.php
- src/Modules/Accounting/AccountingController.php
key-decisions:
- "Legacy fallback liczy netto z brutto/1.23 (nie z `total_net` w bazie, bo issue() zapisuje total_net=total_gross)"
- "Multi-rate paragon = wiele wierszy XLSX z powtorzonymi Numer/Data/Brutto"
- "Stawka VAT brana z order_items.tax_rate; brak fallback na 23.0 dla wysylki"
patterns-established:
- "items_json snapshot moze byc rozszerzany o nowe pola (vat) bez zmiany BC widokow"
- "Eksport XLSX dekoduje items_json zamiast JOIN-owac order_items (zachowuje snapshot pattern)"
duration: ~25min
started: 2026-05-12T22:30:00Z
completed: 2026-05-12T23:00:00Z
---
# Phase 123 Plan 01: Receipts Export VAT Breakdown Summary
**Eksport XLSX paragonow w `/accounting` ma 6 nowych kolumn (Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT) z osobnym wierszem per stawka VAT; legacy paragony dostaja fallback 23%.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25 min |
| Started | 2026-05-12T22:30:00Z |
| Completed | 2026-05-12T23:00:00Z |
| Tasks | 2 completed |
| Files modified | 2 (kod) + 3 (docs/state) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Naglowki XLSX zgodne z wymaganiem | Pass | 6 kolumn, brak Data sprzedazy/Konfiguracja/Nr zamowienia/Nr referencyjny — zweryfikowane przez operatora |
| AC-2: Multi-rate breakdown dla nowych paragonow | Pass (pending live test) | Logika `buildVatBreakdown()` grupuje po `vat`; do potwierdzenia po wystawieniu nowego paragonu z mieszanymi stawkami |
| AC-3: Legacy paragony — fallback 23% | Pass | Operator wyeksportowal kwiecien — pierwotnie net=brutto, vat=0 (bug). Po naprawie liczy net = brutto/1.23, vat = brutto - net |
| AC-4: Snapshot rozszerzony o VAT | Pass (pending live test) | Kod zapisuje `vat` per pozycja i `vat=23.0` dla wysylki; do potwierdzenia po wystawieniu nowego paragonu |
## Accomplishments
- Usunieto z eksportu XLSX kolumny ksiegowo nieprzydatne (Data sprzedazy, Konfiguracja, Nr zamowienia, Nr referencyjny).
- Dodano kolumny Kwota netto / Stawka VAT / Kwota VAT z prawidlowym rozbiciem per stawka.
- Rozszerzono `items_json` snapshot o `vat` per pozycja (forward-compatible — stare widoki bez zmian).
- Naprawiono fallback dla legacy paragonow (nie polega na zlamanym `total_net = total_gross` z `ReceiptService::issue()`).
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `src/Modules/Accounting/ReceiptService.php` | Modified | `buildItemsSnapshot()` zapisuje `vat` per pozycja (z `order_items.tax_rate`, fallback 23.0) |
| `src/Modules/Accounting/AccountingController.php` | Modified | Nowe naglowki + helper `buildVatBreakdown()` + `formatVatRate()`; multi-rate breakdown |
| `.paul/codebase/architecture.md` | Modified | Sekcja "Phase 123 — Receipts Export VAT Breakdown" |
| `.paul/codebase/tech_changelog.md` | Modified | Nowy wpis Phase 123 |
| `.paul/STATE.md` | Modified | Loop position UNIFY |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Legacy fallback liczy `net = brutto/1.23` zamiast brac `total_net` z bazy | `ReceiptService::issue()` zapisuje `total_net = total_gross` (znany bug, poza zakresem 123); branie `total_net` daje vat=0 dla wszystkich starych paragonow | Operator widzi sensowne rozbicie VAT dla historycznych paragonow; oryginalny `total_net` w `receipts` pozostaje nietkniety |
| Multi-rate = wiele wierszy XLSX (nie wieloliniowe komorki) | User wybral ten format (bardziej Excel-friendly do sumowania) | Numer/Data/Brutto powtarzaja sie w wierszach jednego paragonu |
| Pozycja "Koszt wysylki" dostaje `vat=23.0` | Domyslna stawka dla uslugi wysylki w PL | Eksport bedzie spojny dopoki nie pojawi sie potrzeba uslug 8% (wtedy do uzupelnienia) |
| Brak migracji wstecznej `items_json` | Fallback w eksporcie wystarczy; backfill ryzykowny dla snapshotow | Stare paragony wyswietlaja sie w eksporcie z fallbackiem 23%; nowe — z dokladnym breakdown |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | Krytyczne — bez tej naprawy AC-3 by failowal |
| Scope additions | 0 | — |
| Deferred | 1 | RECEIPT-NET-FIX (poza zakresem 123) |
**Total impact:** Jedna naprawa post-APPLY na podstawie smoke testu operatora; reszta zgodnie z planem.
### Auto-fixed Issues
**1. [Bugfix] Legacy fallback dawal net=brutto, vat=0**
- **Found during:** smoke test operatora (eksport kwietnia)
- **Issue:** Plan zakladal `Kwota netto = total_net`, `Kwota VAT = total_gross - total_net`, ale `ReceiptService::issue()` zapisuje `total_net = total_gross` (linie 81-82 ReceiptService) — wiec roznica zawsze = 0.
- **Fix:** Zmieniono fallback na `net = round(total_gross / 1.23, 2)`, `vat = total_gross - net`.
- **Files:** `src/Modules/Accounting/AccountingController.php` (`buildVatBreakdown()`)
- **Verification:** Operator potwierdzil eksportem kwietnia — kwoty teraz sensowne.
### Deferred Items
- **RECEIPT-NET-FIX** — `ReceiptService::issue()` powinien zapisywac realne `total_net` (a nie kopie `total_gross`). Wymaga sumowania per pozycja z VAT. Dodaj do `.paul/codebase/todo.md`.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Legacy fallback dawal net=brutto, vat=0 | Naprawione w `buildVatBreakdown()` — patrz Auto-fixed #1 |
## Next Phase Readiness
**Ready:**
- Eksport XLSX dziala z VAT breakdown dla nowych paragonow.
- Snapshot pattern rozszerzony o `vat` — gotowy na inne raporty (np. eksport per stawka, JPK).
**Concerns:**
- `ReceiptService::issue()` nadal zapisuje `total_net = total_gross` (RECEIPT-NET-FIX) — eksport teraz nie polega na tym polu, ale UI/widoki paragonu moga wyswietlac mylace dane.
**Blockers:** None
---
*Phase: 123-receipts-export-vat-breakdown, Plan: 01*
*Completed: 2026-05-12*