Files
orderPRO/.paul/phases/123-receipts-export-vat-breakdown/123-01-PLAN.md
Jacek Pyziak 0227f2d072 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>
2026-05-12 21:06:53 +02:00

176 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>