From ed057fc304f94da4d76bc293dc127622b79d36aa Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sun, 15 Mar 2026 19:49:06 +0100 Subject: [PATCH] =?UTF-8?q?feat(08-10-receipt-module):=20phases=2008-10=20?= =?UTF-8?q?complete=20=E2=80=94=20receipt=20issuing=20from=20orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 08 — DB Foundation: - 3 new tables: receipt_configs, receipts, receipt_number_counters - company_settings extended with BDO, REGON, KRS, logo fields Phase 09 — Receipt Config: - CRUD for receipt configurations (Settings > Accounting) - ReceiptConfigController + ReceiptConfigRepository Phase 10 — Receipt Issuing: - ReceiptRepository with atomic numbering (INSERT ON DUPLICATE KEY UPDATE) - ReceiptController with snapshot pattern (seller/buyer/items as JSON) - "Wystaw paragon" button in order view - Documents tab showing both receipts and marketplace documents - Activity log entry on receipt creation Co-Authored-By: Claude Opus 4.6 (1M context) --- .paul/PROJECT.md | 11 +- .paul/ROADMAP.md | 12 +- .paul/STATE.md | 44 ++- .paul/phases/08-db-foundation/08-01-PLAN.md | 203 +++++++++++ .../phases/08-db-foundation/08-01-SUMMARY.md | 131 +++++++ .paul/phases/09-receipt-config/09-01-PLAN.md | 308 +++++++++++++++++ .../phases/09-receipt-config/09-01-SUMMARY.md | 126 +++++++ .paul/phases/10-receipt-issue/10-01-PLAN.md | 320 ++++++++++++++++++ .../phases/10-receipt-issue/10-01-SUMMARY.md | 142 ++++++++ DOCS/ARCHITECTURE.md | 42 +++ ...15_000050_create_receipt_configs_table.sql | 13 + .../20260315_000051_create_receipts_table.sql | 23 ++ ...2_create_receipt_number_counters_table.sql | 10 + ...3_extend_company_settings_extra_fields.sql | 5 + public/assets/css/app.css | 18 +- resources/lang/pl.php | 97 ++++++ resources/scss/app.scss | 6 +- resources/scss/shared/_ui-components.scss | 12 +- resources/views/components/table-list.php | 11 +- resources/views/layouts/app.php | 3 + resources/views/orders/receipt-create.php | 104 ++++++ resources/views/orders/show.php | 75 +++- resources/views/settings/accounting.php | 158 +++++++++ resources/views/settings/company.php | 20 ++ routes/web.php | 29 +- src/Modules/Accounting/ReceiptController.php | 263 ++++++++++++++ src/Modules/Accounting/ReceiptRepository.php | 121 +++++++ src/Modules/Orders/OrdersController.php | 20 +- .../Settings/CompanySettingsRepository.php | 16 + .../Settings/ReceiptConfigController.php | 136 ++++++++ .../Settings/ReceiptConfigRepository.php | 99 ++++++ 31 files changed, 2539 insertions(+), 39 deletions(-) create mode 100644 .paul/phases/08-db-foundation/08-01-PLAN.md create mode 100644 .paul/phases/08-db-foundation/08-01-SUMMARY.md create mode 100644 .paul/phases/09-receipt-config/09-01-PLAN.md create mode 100644 .paul/phases/09-receipt-config/09-01-SUMMARY.md create mode 100644 .paul/phases/10-receipt-issue/10-01-PLAN.md create mode 100644 .paul/phases/10-receipt-issue/10-01-SUMMARY.md create mode 100644 database/migrations/20260315_000050_create_receipt_configs_table.sql create mode 100644 database/migrations/20260315_000051_create_receipts_table.sql create mode 100644 database/migrations/20260315_000052_create_receipt_number_counters_table.sql create mode 100644 database/migrations/20260315_000053_extend_company_settings_extra_fields.sql create mode 100644 resources/views/orders/receipt-create.php create mode 100644 resources/views/settings/accounting.php create mode 100644 src/Modules/Accounting/ReceiptController.php create mode 100644 src/Modules/Accounting/ReceiptRepository.php create mode 100644 src/Modules/Settings/ReceiptConfigController.php create mode 100644 src/Modules/Settings/ReceiptConfigRepository.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index fb4d0b8..7c959db 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -27,10 +27,14 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i n - [x] UX: orderpro-to-allegro disable, lista zamówień poprawki — Phase 7 - [x] Unit tests: AllegroTokenManager, AllegroOrderImportService (12 testów) — Phase 7 - [x] InPost ShipX API: natywny provider niezależny od Allegro — Phase 7 +- [x] DB Foundation: tabele receipts, receipt_configs, receipt_number_counters + company_settings extended — Phase 8 +- [x] Konfiguracja paragonów (CRUD w Ustawienia > Księgowość) — Phase 9 +- [x] Wystawianie paragonów z zamówienia (formularz, snapshoty, atomowe numerowanie) — Phase 10 ### Active (In Progress) -- [ ] [Awaiting next milestone definition] +- [ ] Podgląd i wydruk paragonu (HTML+PDF) — Phase 11 +- [ ] Sekcja Księgowość — lista paragonów + eksport XLSX — Phase 12 ### Planned (Next) @@ -86,6 +90,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'ów (Allegro, Erli) oraz API p | dg/bypass-finals do testów final classes | Wszystkie klasy final — mockowanie przez bypass-finals zamiast usuwania final | 2026-03-15 | Active | | InPost ShipX API zamiast Allegro WZA remap | InpostIntegrationRepository jest pod ShipX; niezależność od Allegro | 2026-03-15 | Active | | vendor/ w ftp-kr ignore | Auto-upload dev deps na serwer powodował Fatal Error | 2026-03-15 | Active | +| Snapshot pattern: seller/buyer/items jako JSON w receipts | Dane zamrożone w momencie wystawienia — niezależne od przyszłych zmian źródła | 2026-03-15 | Active | +| Atomowe numerowanie paragonów: INSERT ON DUPLICATE KEY UPDATE | Bezpieczne generowanie kolejnych numerów bez race conditions | 2026-03-15 | Active | +| Moduł Accounting w osobnym namespace | App\Modules\Accounting — separacja od Settings | 2026-03-15 | Active | ## Success Metrics @@ -117,4 +124,4 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-03-15 after Phase 7 (Pre-Expansion Fixes — milestone v0.2 complete)* +*Last updated: 2026-03-15 after Phase 10 (Wystawianie paragonów z zamówienia)* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index bc998dc..c74f22c 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -6,7 +6,17 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz ## Current Milestone -No active milestone. Run `/paul:milestone` to define next. +### v0.3 Moduł Paragonów — In progress + +Moduł księgowości z obsługą paragonów: wielokonfiguracyjne szablony, wystawianie z zamówień, podgląd/wydruk HTML+PDF, lista z filtrami i eksportem XLSX. + +| Phase | Name | Plans | Status | +|-------|------|-------|--------| +| 8 | DB Foundation + Company Settings | 1/1 | Complete ✓ | +| 9 | Konfiguracja paragonów (Ustawienia) | 1/1 | Complete ✓ | +| 10 | Wystawianie paragonów z zamówienia | 1/1 | Complete ✓ | +| 11 | Podgląd i wydruk paragonu (HTML+PDF) | 1 | Not started | +| 12 | Sekcja Księgowość — lista + eksport XLSX | 1 | Not started | ## Completed Milestones diff --git a/.paul/STATE.md b/.paul/STATE.md index 6659c00..1b12f35 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,26 +5,31 @@ See: .paul/PROJECT.md (updated 2026-03-12) **Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami. -**Current focus:** Milestone v0.2 COMPLETE. Następny milestone do zdefiniowania. +**Current focus:** Milestone v0.3 Moduł Paragonów — Faza 11 Podgląd i wydruk paragonu. ## Current Position -Milestone: v0.2 Pre-Expansion Fixes — COMPLETE ✓ -Phase: 7 of 7 (07-pre-expansion-fixes) — Complete -Plan: 07-01 ✓, 07-02 ✓, 07-03 ✓, 07-04 ✓, 07-05 ✓ -Status: Milestone v0.2 complete — ready for next milestone -Last activity: 2026-03-15 — Phase 07 transition complete, milestone v0.2 done +Milestone: v0.3 Moduł Paragonów +Phase: 10 of 12 (10-receipt-issue) — Complete ✓ +Plan: 10-01 ✓ +Status: Phase 10 complete — ready for Phase 11 +Last activity: 2026-03-15 — Phase 10 loop closed Progress: - v0.1 Initial Release: [██████████] 100% ✓ -- v0.2 Pre-Expansion Fixes: [██████████] 100% (5/5 planów) +- v0.2 Pre-Expansion Fixes: [██████████] 100% ✓ +- v0.3 Moduł Paragonów: [██████░░░░] 60% + - Phase 8: [██████████] 100% ✓ + - Phase 9: [██████████] 100% ✓ + - Phase 10: [██████████] 100% ✓ + - Phase 11: [░░░░░░░░░░] 0% ## Loop Position Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Phase 07 complete — milestone v0.2 done] + ✓ ✓ ✓ [Phase 10 complete — ready for next phase] ``` ## Accumulated Context @@ -42,6 +47,14 @@ PLAN ──▶ APPLY ──▶ UNIFY | 2026-03-15 | 3 bugi use-statement naprawione (odkryte przez testy) | Faza 07 | RuntimeException catch w 401 retry wreszcie działa; AllegroOAuthException rzucane poprawnie | | 2026-03-15 | InPost ShipX API (nie Allegro WZA) jako natywny provider | Faza 07 | InpostShipmentService niezależny od Allegro; workaround remap usunięty | | 2026-03-15 | vendor/ dodany do ftp-kr ignore; deploy vendor ręcznie | Faza 07 | Auto-upload nie nadpisze vendor/ na serwerze | +| 2026-03-15 | Snapshot pattern: seller/buyer/items jako JSON | Faza 10 | Dane zamrożone w momencie wystawienia paragonu | +| 2026-03-15 | Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE | Faza 10 | Bezpieczne kolejne numery paragonów | +| 2026-03-15 | Moduł Accounting w App\Modules\Accounting | Faza 10 | Separacja od Settings | + +### Skill Audit (Faza 10, Plan 01) +| Oczekiwany | Wywołany | Uwagi | +|------------|---------|-------| +| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY | ### Skill Audit (Faza 07, Plan 05) | Oczekiwany | Wywołany | Uwagi | @@ -112,7 +125,7 @@ PLAN ──▶ APPLY ──▶ UNIFY - **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno. ### Git State -Last commit: 5ab87a5 (feat(07-pre-expansion-fixes): complete phase 07 — milestone v0.2 done) +Last commit: pending (phase 10 commit) Branch: main Feature branches merged: none @@ -122,13 +135,14 @@ Brak. ## Session Continuity Last session: 2026-03-15 -Stopped at: Milestone v0.2 complete -Next action: /paul:milestone (define next milestone) -Resume file: .paul/ROADMAP.md +Stopped at: Phase 10 complete +Next action: /paul:plan for Phase 11 (Podglad i wydruk paragonu HTML+PDF) +Resume file: .paul/phases/10-receipt-issue/10-01-SUMMARY.md Resume context: -- v0.1: COMPLETE ✓ (6 phases, 15 plans — tech debt, bugs, quality) -- v0.2: COMPLETE ✓ (1 phase, 5 plans — performance, stability, UX, tests, InPost) -- Next milestone to define +- v0.1: COMPLETE ✓ (6 phases, 15 plans) +- v0.2: COMPLETE ✓ (1 phase, 5 plans) +- v0.3: IN PROGRESS — Phase 08-10 done, Phase 11 next +- Faza 0 (nieaktywne przyciski) zrobiona poza planem --- *STATE.md — Updated after every significant action* diff --git a/.paul/phases/08-db-foundation/08-01-PLAN.md b/.paul/phases/08-db-foundation/08-01-PLAN.md new file mode 100644 index 0000000..41e5fef --- /dev/null +++ b/.paul/phases/08-db-foundation/08-01-PLAN.md @@ -0,0 +1,203 @@ +--- +phase: 08-db-foundation +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - database/migrations/20260315_000050_create_receipt_configs_table.sql + - database/migrations/20260315_000051_create_receipts_table.sql + - database/migrations/20260315_000052_create_receipt_number_counters_table.sql + - database/migrations/20260315_000053_extend_company_settings_extra_fields.sql + - src/Modules/Settings/CompanySettingsRepository.php + - resources/views/settings/company.php + - DOCS/DB_SCHEMA.md + - DOCS/ARCHITECTURE.md +autonomous: false +--- + + +## Goal +Wdrożyć migracje bazodanowe dla modułu paragonów (receipt_configs, receipts, receipt_number_counters) oraz rozszerzyć company_settings o pola wymagane na paragonach (BDO, REGON, KRS, logo). Zaktualizować UI danych firmy o nowe pola. + +## Purpose +Fundament bazodanowy pod cały moduł paragonów — bez tych tabel nie można przejść do logiki konfiguracji ani wystawiania. + +## Output +- 4 pliki migracji SQL (już utworzone, do weryfikacji) +- Rozszerzony formularz danych firmy o pola BDO, REGON, KRS, logo +- Zaktualizowane DOCS/DB_SCHEMA.md i DOCS/ARCHITECTURE.md + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@database/migrations/20260315_000050_create_receipt_configs_table.sql +@database/migrations/20260315_000051_create_receipts_table.sql +@database/migrations/20260315_000052_create_receipt_number_counters_table.sql +@database/migrations/20260315_000053_extend_company_settings_extra_fields.sql +@src/Modules/Settings/CompanySettingsRepository.php +@resources/views/settings/company.php +@DOCS/DB_SCHEMA.md + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | ○ | + +No specialized flows configured for DB migrations. + + + + +## AC-1: Tabele paragonów istnieją po migracji +```gherkin +Given baza danych jest dostępna i migrator działa +When uruchomię migracje pending +Then istnieją tabele receipt_configs, receipts, receipt_number_counters z poprawnymi kolumnami, indeksami i FK +``` + +## AC-2: Company settings rozszerzony o nowe pola +```gherkin +Given tabela company_settings istnieje +When uruchomię migrację 000053 +Then tabela ma kolumny bdo_number, regon, court_register, logo_path +``` + +## AC-3: Formularz danych firmy wyświetla i zapisuje nowe pola +```gherkin +Given użytkownik jest na stronie /settings/company +When wypełni pola BDO, REGON, KRS, logo i kliknie Zapisz +Then dane są zapisane w company_settings i widoczne po odświeżeniu +``` + +## AC-4: Dokumentacja zaktualizowana +```gherkin +Given migracje zostały wdrożone +When przeglądam DOCS/DB_SCHEMA.md i DOCS/ARCHITECTURE.md +Then widzę opisy nowych tabel receipt_configs, receipts, receipt_number_counters oraz nowych kolumn company_settings +``` + + + + + + + Task 1: Weryfikacja i wdrożenie migracji SQL + + database/migrations/20260315_000050_create_receipt_configs_table.sql, + database/migrations/20260315_000051_create_receipts_table.sql, + database/migrations/20260315_000052_create_receipt_number_counters_table.sql, + database/migrations/20260315_000053_extend_company_settings_extra_fields.sql + + + 1. Przejrzeć 4 pliki migracji już utworzonych — zweryfikować poprawność SQL: + - receipt_configs: pola name, is_active, number_format, numbering_type, is_named, sale_date_source, order_reference + - receipts: FK do orders i receipt_configs, UNIQUE na receipt_number, indeksy, JSON kolumny + - receipt_number_counters: UNIQUE na (config_id, year, month), FK do receipt_configs + - company_settings: ADD COLUMN IF NOT EXISTS dla bdo_number, regon, court_register, logo_path + 2. Uruchomić migracje przez UI /settings/database lub bezpośrednio + 3. Zweryfikować struktury tabel po migracji + + Sprawdzić SHOW CREATE TABLE dla receipt_configs, receipts, receipt_number_counters; SHOW COLUMNS FROM company_settings + AC-1 i AC-2 spełnione: tabele istnieją z poprawnymi kolumnami i kluczami + + + + Task 2: Rozszerzenie formularza danych firmy o nowe pola + + src/Modules/Settings/CompanySettingsRepository.php, + resources/views/settings/company.php + + + 1. W CompanySettingsRepository: + - Dodać bdo_number, regon, court_register, logo_path do metody defaults() + - Dodać te pola do saveSettings() (mapowanie z POST) + - Dodać te pola do getSettings() jeśli nie zwracane automatycznie + 2. W resources/views/settings/company.php: + - Dodać 4 nowe pola formularza (input text) w sekcji danych firmy: + - Numer BDO (bdo_number) — varchar 20 + - REGON (regon) — varchar 14 + - KRS / Wpis do ewidencji (court_register) — varchar 128 + - Logo firmy (logo_path) — na razie pole tekstowe ze ścieżką (upload w przyszłości) + - Zachować istniejący styl formularza + 3. Nie zmieniać kontrolera — CompanySettingsController::save() przekazuje wszystkie pola POST do repository + + Odwiedzić /settings/company — zobaczyć nowe pola, wypełnić, zapisać, odświeżyć — dane zachowane + AC-3 spełnione: nowe pola widoczne i zapisywalne + + + + Migracje DB dla modułu paragonów + rozszerzony formularz danych firmy + + 1. Wejdź na /settings/database — uruchom pending migracje + 2. Wejdź na /settings/company — sprawdź czy są nowe pola (BDO, REGON, KRS, Logo) + 3. Wypełnij nowe pola, zapisz, odśwież — dane powinny być zachowane + 4. Potwierdź że istniejące dane firmy nie zniknęły + + Type "approved" to continue, or describe issues to fix + + + + Task 3: Aktualizacja dokumentacji DB_SCHEMA.md i ARCHITECTURE.md + DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md + + 1. W DOCS/DB_SCHEMA.md dodać sekcje: + - ### receipt_configs — z opisem kolumn, indeksów + - ### receipts — z opisem kolumn, FK, indeksów + - ### receipt_number_counters — z opisem kolumn, UNIQUE, FK + - Zaktualizować sekcję company_settings o nowe kolumny + - Dodać wpis chronologiczny w sekcji Status + 2. W DOCS/ARCHITECTURE.md: + - Dodać moduł App\Modules\Accounting (przygotowanie) + - Dodać informację o nowych tabelach w sekcji domen + + Przejrzeć oba pliki — sprawdzić kompletność opisów nowych tabel + AC-4 spełnione: dokumentacja odzwierciedla nowy schemat + + + + + + +## DO NOT CHANGE +- Istniejące tabele (orders, order_items, order_activity_log) — żadnych zmian +- Istniejące kontrolery i widoki niezwiązane z company settings +- Logika istniejących integracji (Allegro, shopPRO, Apaczka, InPost) + +## SCOPE LIMITS +- Nie tworzymy jeszcze kontrolera paragonów (Faza 09) +- Nie tworzymy widoków konfiguracji paragonów (Faza 09) +- Upload logo — na razie tylko pole tekstowe, upload w przyszłości +- Nie dodajemy nawigacji do sekcji Księgowość (Faza 12) + + + + +Before declaring plan complete: +- [ ] 4 migracje wykonane bez błędów +- [ ] Tabele receipt_configs, receipts, receipt_number_counters istnieją z poprawnymi strukturami +- [ ] company_settings ma kolumny bdo_number, regon, court_register, logo_path +- [ ] Formularz /settings/company wyświetla i zapisuje nowe pola +- [ ] DOCS/DB_SCHEMA.md zaktualizowany +- [ ] DOCS/ARCHITECTURE.md zaktualizowany +- [ ] Istniejąca funkcjonalność danych firmy niezmieniona + + + +- Wszystkie 4 migracje wykonane pomyślnie +- Nowe pola w formularzu danych firmy działają (zapis + odczyt) +- Dokumentacja kompletna +- Brak regresji w istniejącej funkcjonalności + + + +After completion, create `.paul/phases/08-db-foundation/08-01-SUMMARY.md` + diff --git a/.paul/phases/08-db-foundation/08-01-SUMMARY.md b/.paul/phases/08-db-foundation/08-01-SUMMARY.md new file mode 100644 index 0000000..aa44d91 --- /dev/null +++ b/.paul/phases/08-db-foundation/08-01-SUMMARY.md @@ -0,0 +1,131 @@ +--- +phase: 08-db-foundation +plan: 01 +subsystem: database +tags: [mysql, migrations, receipts, company-settings] + +requires: [] +provides: + - receipt_configs table + - receipts table + - receipt_number_counters table + - company_settings extended fields (bdo, regon, krs, logo) +affects: [09-receipt-config, 10-receipt-issue, 11-receipt-print, 12-accounting-list] + +tech-stack: + added: [] + patterns: [snapshot-json-pattern for receipts] + +key-files: + created: + - database/migrations/20260315_000050_create_receipt_configs_table.sql + - database/migrations/20260315_000051_create_receipts_table.sql + - database/migrations/20260315_000052_create_receipt_number_counters_table.sql + - database/migrations/20260315_000053_extend_company_settings_extra_fields.sql + modified: + - src/Modules/Settings/CompanySettingsRepository.php + - resources/views/settings/company.php + - DOCS/DB_SCHEMA.md + - DOCS/ARCHITECTURE.md + +key-decisions: + - "receipts.order_id as BIGINT UNSIGNED (match orders.id type)" + - "receipts uses JSON snapshots for seller/buyer/items data" + - "receipt_configs ON DELETE RESTRICT (nie usuwaj konfiguracji z istniejacymi paragonami)" + +patterns-established: + - "Snapshot pattern: dane sprzedawcy/nabywcy/pozycji zapisywane jako JSON w momencie wystawienia" + +duration: ~15min +completed: 2026-03-15 +--- + +# Phase 8 Plan 01: DB Foundation + Company Settings Summary + +**Migracje bazodanowe dla modulu paragonow (3 nowe tabele) + rozszerzenie company_settings o BDO/REGON/KRS/logo.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~15min | +| Completed | 2026-03-15 | +| Tasks | 4 completed | +| Files modified | 8 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Tabele paragonow istnieja po migracji | Pass | receipt_configs, receipts, receipt_number_counters utworzone | +| AC-2: Company settings rozszerzony | Pass | bdo_number, regon, court_register, logo_path dodane | +| AC-3: Formularz wyswietla i zapisuje nowe pola | Pass | Zweryfikowane manualnie przez uzytkownika | +| AC-4: Dokumentacja zaktualizowana | Pass | DB_SCHEMA.md i ARCHITECTURE.md zaktualizowane | + +## Accomplishments + +- 3 nowe tabele bazodanowe dla modulu paragonow (receipt_configs, receipts, receipt_number_counters) +- Rozszerzenie company_settings o 4 pola wymagane na dokumentach ksiegowych +- Formularz danych firmy zaktualizowany o nowe pola z pelnym zapisem/odczytem + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260315_000050_*.sql` | Created | Tabela receipt_configs | +| `database/migrations/20260315_000051_*.sql` | Created | Tabela receipts | +| `database/migrations/20260315_000052_*.sql` | Created | Tabela receipt_number_counters | +| `database/migrations/20260315_000053_*.sql` | Created | Rozszerzenie company_settings | +| `src/Modules/Settings/CompanySettingsRepository.php` | Modified | Dodano 4 nowe pola (get/save/defaults) | +| `resources/views/settings/company.php` | Modified | Dodano pola REGON, BDO, KRS, Logo | +| `DOCS/DB_SCHEMA.md` | Modified | Dokumentacja 3 nowych tabel + nowe kolumny | +| `DOCS/ARCHITECTURE.md` | Modified | Dodano modul Accounting (w budowie) | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| receipts.order_id BIGINT UNSIGNED | orders.id jest BIGINT UNSIGNED — FK musi sie zgadzac | Fix bledu errno 150 | +| ON DELETE RESTRICT dla receipt_configs | Nie mozna usunac konfiguracji jesli istnieja paragony | Bezpieczenstwo danych | +| Logo jako sciezka tekstowa | Upload w przyszlosci, na razie pole tekstowe | Uproszczenie fazy 08 | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 1 | Krytyczny fix typu FK | + +**Total impact:** Jeden fix krytyczny, brak scope creep. + +### Auto-fixed Issues + +**1. FK type mismatch receipts.order_id** +- **Found during:** Task 1 (migracje) +- **Issue:** receipts.order_id bylo INT UNSIGNED, orders.id jest BIGINT UNSIGNED +- **Fix:** Zmieniono na BIGINT UNSIGNED +- **Files:** database/migrations/20260315_000051_create_receipts_table.sql +- **Verification:** Migracja wykonana pomyslnie po poprawce + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| errno 150 FK constraint | Poprawiono typ order_id z INT na BIGINT UNSIGNED | + +## Next Phase Readiness + +**Ready:** +- Tabele bazodanowe gotowe pod CRUD konfiguracji (faza 09) +- Company settings kompletne pod snapshot danych sprzedawcy + +**Concerns:** +- Brak + +**Blockers:** +- Brak + +--- +*Phase: 08-db-foundation, Plan: 01* +*Completed: 2026-03-15* diff --git a/.paul/phases/09-receipt-config/09-01-PLAN.md b/.paul/phases/09-receipt-config/09-01-PLAN.md new file mode 100644 index 0000000..b481474 --- /dev/null +++ b/.paul/phases/09-receipt-config/09-01-PLAN.md @@ -0,0 +1,308 @@ +--- +phase: 09-receipt-config +plan: 01 +type: execute +wave: 1 +depends_on: ["08-01"] +files_modified: + - src/Modules/Settings/ReceiptConfigController.php + - src/Modules/Settings/ReceiptConfigRepository.php + - resources/views/settings/accounting.php + - resources/views/layouts/app.php + - resources/lang/pl.php + - routes/web.php + - DOCS/ARCHITECTURE.md +autonomous: false +--- + + +## Goal +Wdrozyc CRUD konfiguracji paragonow w sekcji Ustawienia > Ksiegowosc: lista konfiguracji, tworzenie, edycja, aktywacja/dezaktywacja, usuwanie. Dodac sublinek "Ksiegowosc" do nawigacji ustawien. + +## Purpose +Umozliwienie zarzadzania szablonami paragonow (numeracja, imiennosc, zrodlo daty sprzedazy) — fundament pod wystawianie paragonow w fazie 10. + +## Output +- ReceiptConfigController (index, save, toggleStatus, delete) +- ReceiptConfigRepository (CRUD na receipt_configs) +- Widok settings/accounting.php z lista i formularzem +- Sublinek w sidebarze Ustawienia +- Tlumaczenia PL + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/08-db-foundation/08-01-SUMMARY.md + +## Source Files (wzorce) +@src/Modules/Settings/CompanySettingsController.php +@src/Modules/Settings/CompanySettingsRepository.php +@resources/views/settings/company.php +@resources/views/layouts/app.php +@routes/web.php +@resources/lang/pl.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | ○ | + + + + +## AC-1: Lista konfiguracji paragonow +```gherkin +Given uzytkownik jest zalogowany +When wchodzi na /settings/accounting +Then widzi liste wszystkich konfiguracji paragonow (nazwa, status, format numeracji, typ) +And widzi przycisk "Dodaj konfiguracje" +``` + +## AC-2: Tworzenie nowej konfiguracji +```gherkin +Given uzytkownik jest na /settings/accounting +When wypelnia formularz (nazwa, format numeracji, typ numeracji, imiennosc, zrodlo daty, referencja zamowienia) i klika Zapisz +Then konfiguracja jest zapisana w receipt_configs +And pojawia sie na liscie z flash message sukcesu +``` + +## AC-3: Edycja istniejącej konfiguracji +```gherkin +Given istnieje konfiguracja paragonow +When uzytkownik klika "Edytuj" przy konfiguracji +Then formularz jest wypelniony danymi konfiguracji +When zmienia dane i klika Zapisz +Then zmiany sa zapisane i widoczne na liscie +``` + +## AC-4: Aktywacja/dezaktywacja konfiguracji +```gherkin +Given istnieje konfiguracja paragonow +When uzytkownik klika przycisk aktywuj/dezaktywuj +Then status konfiguracji zmienia sie (is_active toggle) +And lista odswiezja sie z nowym statusem +``` + +## AC-5: Usuwanie konfiguracji +```gherkin +Given istnieje konfiguracja paragonow bez wystawionych paragonow +When uzytkownik klika "Usun" i potwierdza w OrderProAlerts.confirm() +Then konfiguracja jest usunieta z bazy +And znika z listy +``` + +## AC-6: Nawigacja — sublinek Ksiegowosc +```gherkin +Given uzytkownik jest zalogowany +When patrzy na sidebar w sekcji Ustawienia +Then widzi sublinek "Ksiegowosc" prowadzacy do /settings/accounting +And sublinek jest podswietlony gdy jest na stronie konfiguracji +``` + + + + + + + Task 1: ReceiptConfigRepository — CRUD na receipt_configs + src/Modules/Settings/ReceiptConfigRepository.php + + Utworzyc klase `App\Modules\Settings\ReceiptConfigRepository` wzorowana na CompanySettingsRepository: + - Konstruktor: `private readonly PDO $pdo` + - `listAll(): array` — SELECT * FROM receipt_configs ORDER BY created_at DESC + - `findById(int $id): ?array` — SELECT * WHERE id = :id + - `save(array $data): void` — INSERT lub UPDATE (jesli data['id'] istnieje): + - pola: name, is_active, number_format, numbering_type, is_named, sale_date_source, order_reference + - INSERT: prepared statement z VALUES + - UPDATE: prepared statement z WHERE id = :id + - `toggleStatus(int $id): void` — UPDATE receipt_configs SET is_active = NOT is_active WHERE id = :id + - `delete(int $id): bool` — DELETE FROM receipt_configs WHERE id = :id (zwraca false jesli FK RESTRICT zablokuje) + - Wszystko przez prepared statements (wzorzec z CompanySettingsRepository) + - Klasa final + + Sprawdzic syntax PHP: php -l src/Modules/Settings/ReceiptConfigRepository.php + AC-2, AC-3, AC-4, AC-5 (backend) spelnione + + + + Task 2: ReceiptConfigController — kontroler CRUD + src/Modules/Settings/ReceiptConfigController.php + + Utworzyc klase `App\Modules\Settings\ReceiptConfigController` wzorowana na CompanySettingsController: + - Konstruktor: Template, Translator, AuthService, ReceiptConfigRepository (readonly) + - `index(Request $request): Response`: + - Pobiera liste konfiguracji z repository->listAll() + - Jesli query param `edit` — laduje konfiguracje do edycji (findById) + - Renderuje 'settings/accounting' z layout 'layouts/app' + - Przekazuje: activeMenu='settings', activeSettings='accounting', csrfToken, configs, editConfig, flash messages + - `save(Request $request): Response`: + - Waliduje CSRF (Csrf::validate) + - Waliduje: name wymagane, number_format wymagane i zawiera %N + - Wywoluje repository->save([...]) z danymi z $request->input(...) + - Flash::set('settings.accounting.flash.saved/save_failed') + - Redirect do /settings/accounting + - `toggleStatus(Request $request): Response`: + - Waliduje CSRF i id + - Wywoluje repository->toggleStatus((int) $request->input('id')) + - Redirect do /settings/accounting + - `delete(Request $request): Response`: + - Waliduje CSRF i id + - try: repository->delete((int) $request->input('id')) + - catch (Throwable): flash error (konfiguracja ma powiazane paragony) + - Redirect do /settings/accounting + - Klasa final + - Uzyc Flash::set/get wzorca, walidacja CSRF przez Csrf::validate($request->input('_token')) + + Sprawdzic syntax PHP: php -l src/Modules/Settings/ReceiptConfigController.php + AC-1, AC-2, AC-3, AC-4, AC-5 (kontroler) spelnione + + + + Task 3: Widok settings/accounting.php, routing, nawigacja, tlumaczenia + + resources/views/settings/accounting.php, + resources/views/layouts/app.php, + routes/web.php, + resources/lang/pl.php + + + 1. Utworzyc `resources/views/settings/accounting.php`: + - Sekcja .card z tytulem "Ksiegowosc — Konfiguracje paragonow" + - Flash messages (error/success) + - Tabela lista konfiguracji: + - Kolumny: Nazwa, Format numeracji, Typ, Imienny, Data sprzedazy, Ref. zamowienia, Status, Akcje + - Status: badge aktywny/nieaktywny + - Akcje: Edytuj (link z ?edit=ID), Aktywuj/Dezaktywuj (form POST), Usun (form POST z OrderProAlerts.confirm) + - Formularz dodawania/edycji (pod tabelą lub w osobnej sekcji): + - hidden: _token, id (jesli edycja) + - Nazwa (text, required) + - Format numeracji (text, required, placeholder: PAR/%N/%M/%Y) + - Typ numeracji (select: miesięczny/roczny) + - Imienny (checkbox) + - Domyslna data sprzedazy (select: wg daty zamowienia/oplacenia/wystawienia) + - Informacja o zamowieniu (select: brak/numer orderPRO/numer integracji) + - Przycisk Zapisz + - Styl: uzyc istniejacych klas .card, .form-control, .form-field, .btn, .form-grid-2, table.data-table + - Usun potwierdzenie: window.OrderProAlerts.confirm() (NIE natywny confirm()) + + 2. W `resources/views/layouts/app.php`: + - Dodac sublinek "Ksiegowosc" po "Dane firmy" w sekcji settings: + ```php + + + + ``` + + 3. W `routes/web.php`: + - Instantiacja: new ReceiptConfigRepository($app->db()), new ReceiptConfigController($template, $translator, $auth, $receiptConfigRepository) + - Trasy: + - GET /settings/accounting -> [$receiptConfigController, 'index'], [$authMiddleware] + - POST /settings/accounting/save -> [$receiptConfigController, 'save'], [$authMiddleware] + - POST /settings/accounting/toggle -> [$receiptConfigController, 'toggleStatus'], [$authMiddleware] + - POST /settings/accounting/delete -> [$receiptConfigController, 'delete'], [$authMiddleware] + - Dodac use statement dla nowych klas + + 4. W `resources/lang/pl.php`: + - navigation.accounting => 'Ksiegowosc' + - settings.accounting.title => 'Konfiguracje paragonow' + - settings.accounting.description => 'Zarzadzaj szablonami numeracji i ustawieniami paragonow' + - settings.accounting.fields.* => tlumaczenia pol formularza + - settings.accounting.flash.saved => 'Konfiguracja zapisana' + - settings.accounting.flash.save_failed => 'Blad zapisu konfiguracji' + - settings.accounting.flash.deleted => 'Konfiguracja usunieta' + - settings.accounting.flash.delete_failed => 'Nie mozna usunac — konfiguracja ma powiazane paragony' + - settings.accounting.flash.toggled => 'Status zmieniony' + - settings.accounting.actions.* => etykiety przyciskow + - settings.accounting.table.* => naglowki kolumn tabeli + - settings.accounting.options.* => opcje selectow (miesięczny/roczny, daty, referencje) + + + Sprawdzic syntax: php -l resources/views/settings/accounting.php + Sprawdzic syntax routes: php -l routes/web.php + + AC-1, AC-6 spelnione + + + + CRUD konfiguracji paragonow w Ustawienia > Ksiegowosc + + 1. Wejdz na strone — w sidebarze Ustawienia powinien byc sublinek "Ksiegowosc" + 2. Kliknij — powinna wyswietlic sie strona /settings/accounting z pusta lista i formularzem + 3. Dodaj nowa konfiguracje: nazwa "Paragony standardowe", format "PAR/%N/%M/%Y", typ miesięczny + 4. Sprawdz czy pojawila sie na liscie + 5. Kliknij Edytuj — formularz powinien sie wypelnic danymi + 6. Zmien nazwe i zapisz — sprawdz aktualizacje + 7. Kliknij Dezaktywuj — status powinien sie zmienic + 8. Kliknij Usun — powinien pojawic sie modal potwierdzenia (OrderProAlerts) + 9. Potwierdz — konfiguracja powinna zniknac z listy + + Type "approved" to continue, or describe issues to fix + + + + Task 4: Aktualizacja ARCHITECTURE.md + DOCS/ARCHITECTURE.md + + Dodac w ARCHITECTURE.md: + 1. W sekcji "Kluczowe klasy": + - App\Modules\Settings\ReceiptConfigController + - App\Modules\Settings\ReceiptConfigRepository + 2. W sekcji "Routing" nowe trasy: + - GET /settings/accounting + - POST /settings/accounting/save + - POST /settings/accounting/toggle + - POST /settings/accounting/delete + 3. Nowa sekcja "Przeplyw Ustawienia > Ksiegowosc" opisujaca CRUD konfiguracji + 4. W sekcji "Nawigacja ustawien" dodac sublinek Ksiegowosc + + Przejrzec ARCHITECTURE.md pod katem kompletnosci + AC-1 do AC-6 udokumentowane + + + + + + +## DO NOT CHANGE +- database/migrations/* — schemat zablokowany (migracje z fazy 08) +- Istniejace kontrolery i widoki niezwiazane z accounting +- src/Modules/Settings/CompanySettingsController.php +- src/Modules/Settings/CompanySettingsRepository.php + +## SCOPE LIMITS +- Nie tworzymy sekcji glownej Ksiegowosc w sidebarze (to faza 12) +- Nie tworzymy logiki wystawiania paragonow (faza 10) +- Nie dodajemy walidacji unikalnosci nazwy konfiguracji (nice-to-have) +- Nie dodajemy sortowania/filtrowania listy konfiguracji (prostota) + + + + +Before declaring plan complete: +- [ ] ReceiptConfigRepository — CRUD dziala (list, find, save, toggle, delete) +- [ ] ReceiptConfigController — 4 endpointy dzialaja +- [ ] Widok accounting.php renderuje liste i formularz +- [ ] Sublinek "Ksiegowosc" widoczny w sidebarze ustawien +- [ ] Tlumaczenia PL kompletne +- [ ] Routing zarejestrowany i dziala +- [ ] OrderProAlerts.confirm() dla usuwania +- [ ] ARCHITECTURE.md zaktualizowany + + + +- Uzytkownik moze tworzyc, edytowac, aktywowac/dezaktywowac i usuwac konfiguracje paragonow +- Nawigacja w sidebarze dziala poprawnie +- Brak regresji w istniejacych ustawieniach + + + +After completion, create `.paul/phases/09-receipt-config/09-01-SUMMARY.md` + diff --git a/.paul/phases/09-receipt-config/09-01-SUMMARY.md b/.paul/phases/09-receipt-config/09-01-SUMMARY.md new file mode 100644 index 0000000..6976f96 --- /dev/null +++ b/.paul/phases/09-receipt-config/09-01-SUMMARY.md @@ -0,0 +1,126 @@ +--- +phase: 09-receipt-config +plan: 01 +subsystem: ui +tags: [php, settings, receipts, crud, scss] + +requires: + - phase: 08-db-foundation + provides: receipt_configs table +provides: + - CRUD konfiguracji paragonow (Ustawienia > Ksiegowosc) + - ReceiptConfigController + ReceiptConfigRepository +affects: [10-receipt-issue, 11-receipt-print, 12-accounting-list] + +tech-stack: + added: [] + patterns: [settings-controller-pattern for new modules] + +key-files: + created: + - src/Modules/Settings/ReceiptConfigController.php + - src/Modules/Settings/ReceiptConfigRepository.php + - resources/views/settings/accounting.php + modified: + - resources/views/layouts/app.php + - routes/web.php + - resources/lang/pl.php + - resources/scss/shared/_ui-components.scss + - resources/scss/app.scss + - public/assets/css/app.css + - DOCS/ARCHITECTURE.md + +key-decisions: + - "Request::input() zamiast query() — klasa Request nie ma metody query()" + - "Csrf::token() i Csrf::validate() jako bool — nie rzuca wyjatkow" + - "Tabela .table zamiast .data-table — data-table nie istnieje w SCSS" + - "form-grid align-items: start — zapobiega rozciaganiu pol przez hint/small" + +patterns-established: + - "Nowe strony ustawien: wzorzec ReceiptConfigController (index+save+toggle+delete)" + +duration: ~30min +completed: 2026-03-15 +--- + +# Phase 9 Plan 01: Konfiguracja paragonow (Ustawienia > Ksiegowosc) Summary + +**CRUD konfiguracji paragonow z nawigacja, tlumaczeniami i poprawkami globalnymi CSS (form-control, form-grid align).** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~30min | +| Completed | 2026-03-15 | +| Tasks | 5 completed | +| Files modified | 10 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Lista konfiguracji | Pass | Tabela z kolumnami Nazwa/Format/Typ/Status/Akcje | +| AC-2: Tworzenie konfiguracji | Pass | Formularz z walidacja nazwy i formatu | +| AC-3: Edycja konfiguracji | Pass | Link ?edit=ID wypelnia formularz | +| AC-4: Toggle aktywnosci | Pass | POST /settings/accounting/toggle | +| AC-5: Usuwanie z potwierdzeniem | Pass | OrderProAlerts.confirm() | +| AC-6: Sublinek Ksiegowosc | Pass | Widoczny i aktywny w sidebarze | + +## Accomplishments + +- Pelny CRUD konfiguracji paragonow z walidacja i flash messages +- Sublinek "Ksiegowosc" w nawigacji ustawien +- Globalne poprawki CSS: form-control kompaktniejszy, form-grid align-items: start + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Settings/ReceiptConfigRepository.php` | Created | CRUD na receipt_configs | +| `src/Modules/Settings/ReceiptConfigController.php` | Created | 4 endpointy (index/save/toggle/delete) | +| `resources/views/settings/accounting.php` | Created | Lista + formularz konfiguracji | +| `resources/views/layouts/app.php` | Modified | Sublinek Ksiegowosc w sidebarze | +| `routes/web.php` | Modified | 4 trasy + use statements | +| `resources/lang/pl.php` | Modified | Tlumaczenia settings.accounting.* | +| `resources/scss/shared/_ui-components.scss` | Modified | form-control: min-height 30px, border-radius 6px | +| `resources/scss/app.scss` | Modified | form-grid-2/3/4: align-items start | +| `public/assets/css/app.css` | Modified | Build CSS | +| `DOCS/ARCHITECTURE.md` | Modified | Nowe trasy, klasy, przeplyw | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 4 | Krytyczne fixy API kontrolera + CSS | + +### Auto-fixed Issues + +**1. Request::query() nie istnieje** +- **Fix:** Zamieniono na Request::input() ktory obsluguje tez query params + +**2. Csrf::generate() nie istnieje** +- **Fix:** Zamieniono na Csrf::token() + +**3. Csrf::validate() zwraca bool, nie rzuca wyjatku** +- **Fix:** Zamieniono try/catch na if(!Csrf::validate()) + +**4. Klasa CSS data-table nie istnieje** +- **Fix:** Zamieniono na .table (istniejaca w _ui-components.scss) + +## Next Phase Readiness + +**Ready:** +- CRUD konfiguracji dziala — faza 10 moze pobierac aktywne konfiguracje do wystawiania paragonow + +**Concerns:** +- Brak + +**Blockers:** +- Brak + +--- +*Phase: 09-receipt-config, Plan: 01* +*Completed: 2026-03-15* diff --git a/.paul/phases/10-receipt-issue/10-01-PLAN.md b/.paul/phases/10-receipt-issue/10-01-PLAN.md new file mode 100644 index 0000000..1a3848b --- /dev/null +++ b/.paul/phases/10-receipt-issue/10-01-PLAN.md @@ -0,0 +1,320 @@ +--- +phase: 10-receipt-issue +plan: 01 +type: execute +wave: 1 +depends_on: ["09-01"] +files_modified: + - src/Modules/Accounting/ReceiptRepository.php + - src/Modules/Accounting/ReceiptController.php + - src/Modules/Orders/OrdersController.php + - resources/views/orders/show.php + - resources/views/orders/receipt-create.php + - routes/web.php + - resources/lang/pl.php + - resources/scss/shared/_ui-components.scss + - public/assets/css/app.css + - DOCS/ARCHITECTURE.md +autonomous: false +--- + + +## Goal +Umozliwienie wystawiania paragonow z poziomu widoku zamowienia — przycisk "Wystaw paragon", formularz wyboru konfiguracji z podgladem pozycji, zapis do bazy z atomowym numerowaniem i snapshotem danych. + +## Purpose +Kluczowa funkcjonalnosc modulu Ksiegowosci (v0.3) — sprzedawca moze wystawic paragon bezposrednio z zamowienia, bez opuszczania widoku zamowienia. + +## Output +- `ReceiptRepository` — CRUD na `receipts` + atomowe numerowanie przez `receipt_number_counters` +- `ReceiptController` — formularz tworzenia paragonu + zapis +- Przycisk "Wystaw paragon" w widoku zamowienia +- Lista wystawionych paragonow w zakladce "Dokumenty" + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/08-db-foundation/08-01-SUMMARY.md — schemat tabel receipts, receipt_configs, receipt_number_counters +@.paul/phases/09-receipt-config/09-01-SUMMARY.md — CRUD konfiguracji, ReceiptConfigRepository, wzorce Request/Csrf + +## Source Files +@database/migrations/20260315_000051_create_receipts_table.sql +@database/migrations/20260315_000052_create_receipt_number_counters_table.sql +@src/Modules/Settings/ReceiptConfigRepository.php +@src/Modules/Settings/CompanySettingsRepository.php +@src/Modules/Orders/OrdersController.php +@resources/views/orders/show.php +@routes/web.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | ○ | + +## Skill Invocation Checklist +- [ ] sonar-scanner uruchomiony po APPLY + + + + +## AC-1: Przycisk "Wystaw paragon" w widoku zamowienia +```gherkin +Given uzytkownik jest na stronie szczegulow zamowienia /orders/{id} +When sa aktywne konfiguracje paragonow +Then widoczny jest przycisk "Wystaw paragon" w sekcji akcji +And przycisk prowadzi do formularza /orders/{id}/receipt/create +``` + +## AC-2: Formularz wystawiania paragonu +```gherkin +Given uzytkownik otwiera /orders/{id}/receipt/create +When formularz sie laduje +Then widoczny jest select z aktywnymi konfiguracjami paragonow +And wyswietlona jest tabela pozycji zamowienia (nazwa, ilosc, cena, suma) +And wyswietlone sa pola: data wystawienia (domyslnie dzis), data sprzedazy (wg konfiguracji) +And widoczny jest podglad danych sprzedawcy z company_settings +And przycisk "Wystaw paragon" submituje formularz +``` + +## AC-3: Zapis paragonu z atomowym numerowaniem +```gherkin +Given uzytkownik wypelnia formularz i klika "Wystaw paragon" +When POST /orders/{id}/receipt/store jest wysylany +Then tworzony jest rekord w tabeli receipts z: + - receipt_number wygenerowanym atomowo z receipt_number_counters (INSERT ON DUPLICATE KEY UPDATE) + - seller_data_json jako snapshot company_settings + - items_json jako snapshot pozycji zamowienia + - total_net i total_gross obliczone z pozycji + - sale_date okreslona wg sale_date_source z konfiguracji + - order_reference_value wypelnione wg order_reference z konfiguracji +And uzytkownik jest przekierowany na /orders/{id} z flash success +``` + +## AC-4: Lista paragonow w zakladce Dokumenty +```gherkin +Given zamowienie ma wystawione paragony +When uzytkownik klika zakladke "Dokumenty" +Then wyswietlona jest tabela paragonow (numer, data wystawienia, kwota brutto, konfiguracja) +And kazdy paragon ma link do podgladu (na razie placeholder — faza 11) +``` + +## AC-5: Walidacja — brak duplikatow i brak pustych konfiguracji +```gherkin +Given uzytkownik probuje wystawic paragon +When nie ma aktywnych konfiguracji +Then przycisk "Wystaw paragon" nie jest widoczny w widoku zamowienia + +Given uzytkownik submituje formularz bez wybranej konfiguracji +When POST jest wysylany +Then zwracany jest blad walidacji i paragon nie jest tworzony +``` + + + + + + + Task 1: ReceiptRepository — CRUD + atomowe numerowanie + src/Modules/Accounting/ReceiptRepository.php + + Utworz nowy modul `src/Modules/Accounting/` z klasa `ReceiptRepository`: + + 1. **Konstruktor:** przyjmuje `PDO $pdo` + 2. **getNextNumber(int $configId, string $numberFormat, string $numberingType): string** + - Atomowe numerowanie przez `INSERT INTO receipt_number_counters (config_id, year, month, last_number) VALUES (:config_id, :year, :month, 1) ON DUPLICATE KEY UPDATE last_number = last_number + 1` + - Odczyt `last_number` przez `SELECT last_number FROM receipt_number_counters WHERE config_id = :config_id AND year = :year AND month = :month` + - Dla `numbering_type = 'yearly'`: month = NULL (w unique key) + - Dla `numbering_type = 'monthly'`: month = biezacy miesiac + - Podmiana w formacie: `%N` → numer (z zerem wiodacym min 3 cyfry), `%M` → miesiac (2 cyfry), `%Y` → rok (4 cyfry) + 3. **create(array $data): int** + - INSERT do `receipts` ze wszystkimi polami + - Zwraca `lastInsertId()` + 4. **findByOrderId(int $orderId): array** + - SELECT receipts + LEFT JOIN receipt_configs (na nazwe konfiguracji) + - ORDER BY created_at DESC + 5. **findById(int $id): ?array** + - SELECT * WHERE id = :id + + Wzorzec: analogicznie do ReceiptConfigRepository (PDO, prepared statements, strict types). + Namespace: `App\Modules\Accounting` + + Klasa parsuje sie bez bledow: `php -l src/Modules/Accounting/ReceiptRepository.php` + AC-3 backend spelnione: atomowe numerowanie i zapis paragonu + + + + Task 2: ReceiptController + routing + widok formularza + + src/Modules/Accounting/ReceiptController.php, + resources/views/orders/receipt-create.php, + routes/web.php, + resources/lang/pl.php + + + 1. **ReceiptController** (`App\Modules\Accounting`): + - Konstruktor: `Template, Translator, AuthService, ReceiptRepository, ReceiptConfigRepository, CompanySettingsRepository, OrdersRepository` + - **create(Request $request): Response** — GET /orders/{id}/receipt/create + - Pobierz zamowienie przez OrdersRepository::findDetails($orderId) + - Pobierz aktywne konfiguracje: ReceiptConfigRepository::listAll() + filtruj is_active = 1 + - Pobierz dane sprzedawcy: CompanySettingsRepository::getSettings() + - Renderuj widok `orders/receipt-create` + - **store(Request $request): Response** — POST /orders/{id}/receipt/store + - Walidacja: config_id wymagane, zamowienie istnieje + - CSRF: Csrf::validate() + - Pobierz konfiguracje (findById), zamowienie (findDetails), company settings + - Oblicz sale_date wg `sale_date_source`: order_date → ordered_at, payment_date → z payments, issue_date → dzis + - Oblicz order_reference_value wg `order_reference`: none → NULL, orderpro → internal_order_number, integration → external_order_id + - Zbuduj seller_data_json z company_settings (company_name, tax_number, street, city, postal_code, phone, email, bank_account, bdo_number, regon, court_register) + - Zbuduj buyer_data_json z address (invoice lub customer) + - Zbuduj items_json z pozycji zamowienia (original_name, quantity, original_price_with_tax, sku, ean) + - Oblicz total_gross = suma(qty * price), total_net = total_gross (paragony nie rozdzielaja netto/brutto — wartosc taka sama) + - Wygeneruj numer przez ReceiptRepository::getNextNumber() + - ReceiptRepository::create() ze wszystkimi danymi + - Flash::set('order.success', 'Paragon wystawiony: ' . $receiptNumber) + - Redirect /orders/{id} + + 2. **Widok receipt-create.php:** + - Layout z naglowkiem "Wystaw paragon" + link powrotny do zamowienia + - Select z aktywnymi konfiguracjami (name + number_format) + - Tabela pozycji zamowienia (readonly): nazwa, ilosc, cena, suma + - Podsumowanie kwoty brutto + - Podglad danych sprzedawcy (readonly z company_settings) + - Pole daty wystawienia (input date, domyslnie dzis) + - Przycisk "Wystaw paragon" + CSRF token + + 3. **routes/web.php:** + - Dodaj use statements: `ReceiptController`, `ReceiptRepository` (z `App\Modules\Accounting`) + - Utworz instancje: `$receiptRepository = new ReceiptRepository($app->db())` + - Utworz kontroler: `$receiptController = new ReceiptController($template, $translator, $auth, $receiptRepository, $receiptConfigRepository, $companySettingsRepository, new OrdersRepository($app->db()))` + - Zarejestruj trasy: + - `GET /orders/{id}/receipt/create` → `[$receiptController, 'create']` + - `POST /orders/{id}/receipt/store` → `[$receiptController, 'store']` + + 4. **resources/lang/pl.php:** + - Dodaj klucze `receipts.create.title`, `receipts.create.select_config`, `receipts.create.issue_date`, `receipts.create.submit`, `receipts.create.seller_data`, `receipts.create.items`, `receipts.create.total`, `receipts.create.back` + + Wzorzec kontrolera: analogicznie do ReceiptConfigController (Csrf::token(), Csrf::validate(), Flash::set/get, Request::input()). + NIE uzywaj natywnych alert()/confirm() — OrderProAlerts juz jest w uzyciu. + + + - `php -l src/Modules/Accounting/ReceiptController.php` + - `php -l resources/views/orders/receipt-create.php` + - Otworz /orders/{id}/receipt/create w przegladarce — formularz sie wyswietla + + AC-2, AC-3, AC-5 spelnione: formularz, zapis, walidacja + + + + Task 3: Integracja z widokiem zamowienia — przycisk + zakladka Dokumenty + + src/Modules/Orders/OrdersController.php, + resources/views/orders/show.php, + routes/web.php + + + 1. **OrdersController::show():** + - Dodaj zaleznosc: `ReceiptConfigRepository` i `ReceiptRepository` (przez konstruktor lub przekazanie w widoku) + - UWAGA: OrdersController ma juz 5 parametrow konstruktora. Zamiast rozszerzac konstruktor, przekaz dane przez nowe instancje w routes/web.php: + - W web.php: pobierz aktywne configs i receipts w zamknieciu routera GET /orders/{id} LUB + - Prostsze: dodaj `?ReceiptRepository` i `?ReceiptConfigRepository` do konstruktora OrdersController + - W metodzie show(): pobierz `$receiptConfigs = $this->receiptConfigs->listAll()` (filtruj aktywne) + - Pobierz `$receipts = $this->receipts->findByOrderId($orderId)` + - Przekaz do widoku: `'receiptConfigs' => $activeConfigs, 'receipts' => $receipts` + + 2. **orders/show.php — przycisk "Wystaw paragon":** + - W sekcji `.order-details-actions` (linia ~47-54): + - Dodaj przycisk miedzy "Przygotuj przesylke" a "Platnosc": + ```php + + Wystaw paragon + + ``` + - Przycisk widoczny tylko gdy sa aktywne konfiguracje (AC-1, AC-5) + + 3. **orders/show.php — zakladka Dokumenty:** + - Zastap pusty placeholder w `data-order-tab-panel="documents"` (linia ~516-521): + - Tabela paragonow: Numer, Data wystawienia, Kwota brutto, Konfiguracja, Akcje + - Jesli brak paragonow: "Brak dokumentow" + - Akcja: link "Podglad" (na razie `#` — placeholder do fazy 11) + - Zaktualizuj licznik w zakladce: `Dokumenty (N)` gdzie N = count($receipts) + + 4. **routes/web.php:** + - Dodaj `use App\Modules\Accounting\ReceiptRepository;` na gorze + - Przekaz `$receiptRepository` i `$receiptConfigRepository` do konstruktora `$ordersController` + + 5. **DOCS/ARCHITECTURE.md:** + - Dodaj modul Accounting z klasami ReceiptRepository, ReceiptController + - Dodaj trasy GET/POST /orders/{id}/receipt/* + + + - Otworz /orders/{id} — przycisk "Wystaw paragon" widoczny (jesli sa aktywne configs) + - Kliknij zakladke "Dokumenty" — tabela paragonow wyswietla sie + - Po wystawieniu paragonu — pojawia sie na liscie w zakladce Dokumenty + + AC-1, AC-4 spelnione: przycisk w widoku zamowienia + lista paragonow w Dokumentach + + + + Wystawianie paragonow z poziomu zamowienia — pelny flow: przycisk → formularz → zapis → lista w Dokumentach + + 1. Otworz dowolne zamowienie /orders/{id} + 2. Sprawdz: przycisk "Wystaw paragon" jest widoczny w akcjach + 3. Kliknij "Wystaw paragon" — formularz sie otwiera + 4. Wybierz konfiguracje, sprawdz podglad pozycji i danych sprzedawcy + 5. Kliknij "Wystaw paragon" w formularzu + 6. Sprawdz: redirect na zamowienie z flash "Paragon wystawiony: PAR/001/03/2026" + 7. Kliknij zakladke "Dokumenty" — paragon widoczny na liscie + 8. Wystaw drugi paragon — numer powinien byc PAR/002/03/2026 + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- database/migrations/* (schemat zablokowany — tabele juz istnieja z fazy 08) +- src/Modules/Settings/ReceiptConfigRepository.php (gotowe z fazy 09) +- src/Modules/Settings/ReceiptConfigController.php (gotowe z fazy 09) +- resources/views/settings/accounting.php (gotowe z fazy 09) + +## SCOPE LIMITS +- Brak podgladu/wydruku paragonu — to faza 11 +- Brak edycji/anulowania paragonu — poza zakresem v0.3 +- Brak generowania PDF — to faza 11 +- Paragony nie rozdzielaja netto/brutto (total_net = total_gross) — uproszczenie dla paragonow + + + + +Before declaring plan complete: +- [ ] `php -l src/Modules/Accounting/ReceiptRepository.php` — brak bledow +- [ ] `php -l src/Modules/Accounting/ReceiptController.php` — brak bledow +- [ ] Przycisk "Wystaw paragon" widoczny w /orders/{id} gdy sa aktywne konfiguracje +- [ ] Formularz /orders/{id}/receipt/create wyswietla pozycje zamowienia i konfiguracje +- [ ] Po wystawieniu paragonu: redirect + flash + rekord w bazie +- [ ] Numer paragonu generowany atomowo (kolejne numery nie powtarzaja sie) +- [ ] Zakladka Dokumenty wyswietla wystawione paragony +- [ ] Brak bledow w konsoli PHP +- [ ] All acceptance criteria met + + + +- Wszystkie 3 taski auto + 1 checkpoint ukonczone +- Wszystkie AC-1 do AC-5 spelnione +- Brak bledow PHP (php -l na wszystkich nowych plikach) +- Weryfikacja manualna przez uzytkownika (checkpoint) + + + +After completion, create `.paul/phases/10-receipt-issue/10-01-SUMMARY.md` + diff --git a/.paul/phases/10-receipt-issue/10-01-SUMMARY.md b/.paul/phases/10-receipt-issue/10-01-SUMMARY.md new file mode 100644 index 0000000..82d7598 --- /dev/null +++ b/.paul/phases/10-receipt-issue/10-01-SUMMARY.md @@ -0,0 +1,142 @@ +--- +phase: 10-receipt-issue +plan: 01 +subsystem: accounting +tags: [php, receipts, orders, crud, snapshots, atomic-numbering] + +requires: + - phase: 08-db-foundation + provides: receipts, receipt_configs, receipt_number_counters tables + - phase: 09-receipt-config + provides: ReceiptConfigRepository CRUD, active configs +provides: + - Wystawianie paragonow z poziomu zamowienia + - ReceiptRepository (CRUD + atomowe numerowanie) + - ReceiptController (formularz + zapis ze snapshotami) + - Przycisk "Wystaw paragon" w widoku zamowienia + - Lista paragonow + dokumentow zewnetrznych w zakladce Dokumenty + - Activity log entry po wystawieniu paragonu +affects: [11-receipt-print, 12-accounting-list] + +tech-stack: + added: [] + patterns: [snapshot-json-pattern for seller/buyer/items, atomic-counter-pattern for receipt numbering] + +key-files: + created: + - src/Modules/Accounting/ReceiptRepository.php + - src/Modules/Accounting/ReceiptController.php + - resources/views/orders/receipt-create.php + modified: + - src/Modules/Orders/OrdersController.php + - resources/views/orders/show.php + - routes/web.php + - resources/lang/pl.php + - DOCS/ARCHITECTURE.md + +key-decisions: + - "ReceiptRepository w App\\Modules\\Accounting (nowy modul, nie w Settings)" + - "Snapshot pattern: seller/buyer/items jako JSON w momencie wystawienia" + - "Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE na receipt_number_counters" + - "total_net = total_gross (paragony nie rozdzielaja netto/brutto)" + - "OrdersController rozszerzony o opcjonalne ?ReceiptRepository i ?ReceiptConfigRepository" + +patterns-established: + - "Modul Accounting: osobny namespace dla funkcjonalnosci ksiegowych" + - "Snapshot przy tworzeniu dokumentu: dane zamrazane w JSON, niezalezne od przyszlych zmian zrodla" + - "Activity log: recordActivity() po kazdej akcji generujacej dokument" + +duration: ~25min +completed: 2026-03-15 +--- + +# Phase 10 Plan 01: Wystawianie paragonow z zamowienia Summary + +**Pelny flow wystawiania paragonow: przycisk w zamowieniu, formularz z podgladem pozycji/sprzedawcy, zapis z atomowym numerowaniem i snapshotami, lista w zakladce Dokumenty + wpis w historii.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~25min | +| Completed | 2026-03-15 | +| Tasks | 3 auto + 1 checkpoint | +| Files created | 3 | +| Files modified | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Przycisk "Wystaw paragon" w widoku zamowienia | Pass | Widoczny tylko gdy sa aktywne konfiguracje | +| AC-2: Formularz wystawiania paragonu | Pass | Select konfiguracji, tabela pozycji, podglad sprzedawcy, data wystawienia | +| AC-3: Zapis paragonu z atomowym numerowaniem | Pass | INSERT ON DUPLICATE KEY UPDATE, snapshoty JSON | +| AC-4: Lista paragonow w zakladce Dokumenty | Pass | Paragony + dokumenty zewnetrzne w osobnych sekcjach | +| AC-5: Walidacja — brak duplikatow i brak pustych konfiguracji | Pass | Przycisk ukryty bez konfiguracji, walidacja config_id | + +## Accomplishments + +- Nowy modul `App\Modules\Accounting` z ReceiptRepository i ReceiptController +- Atomowe numerowanie paragonow przez receipt_number_counters (INSERT ON DUPLICATE KEY UPDATE) +- Snapshoty seller/buyer/items jako JSON — dane zamrozone w momencie wystawienia +- Zakladka Dokumenty wyswietla zarowno paragony jak i dokumenty zewnetrzne z marketplace + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Accounting/ReceiptRepository.php` | Created | CRUD na receipts + atomowe numerowanie | +| `src/Modules/Accounting/ReceiptController.php` | Created | Formularz tworzenia + zapis paragonu + activity log | +| `resources/views/orders/receipt-create.php` | Created | Widok formularza wystawiania paragonu | +| `src/Modules/Orders/OrdersController.php` | Modified | Dodano ?ReceiptRepository, ?ReceiptConfigRepository do konstruktora + show() | +| `resources/views/orders/show.php` | Modified | Przycisk "Wystaw paragon", zakladka Dokumenty z paragony + dokumenty zewnetrzne | +| `routes/web.php` | Modified | Instancje ReceiptRepository/ReceiptController, 2 nowe trasy | +| `resources/lang/pl.php` | Modified | Tlumaczenia receipts.create.*, receipts.documents.*, receipt_issued | +| `DOCS/ARCHITECTURE.md` | Modified | Klasy Accounting, przeplyw wystawiania paragonu | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Scope additions | 2 | User feedback — activity log + dokumenty zewnetrzne | + +**Total impact:** Dwa uzasadnione rozszerzenia poza planem, poprawiajace UX. + +### Scope Additions + +**1. Activity log po wystawieniu paragonu** +- **Source:** User feedback podczas checkpoint +- **Issue:** Brak wpisu w historii zmian zamowienia po wystawieniu paragonu +- **Fix:** Dodano recordActivity() z typem `receipt_issued` + tlumaczenie +- **Files:** ReceiptController.php, resources/lang/pl.php + +**2. Dokumenty zewnetrzne w zakladce Dokumenty** +- **Source:** User feedback — licznik Dokumenty(2) ale widoczny tylko 1 paragon +- **Issue:** Zakladka wyswietlala tylko paragony, nie dokumenty z order_documents +- **Fix:** Dodano sekcje "Dokumenty zewnetrzne" z tabela order_documents +- **Files:** resources/views/orders/show.php + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Duplikacja instancji ReceiptConfigRepository/ReceiptRepository w web.php | Przeniesiono tworzenie przed ordersController, usunieto duplikaty | + +## Next Phase Readiness + +**Ready:** +- ReceiptRepository::findById() gotowe pod podglad paragonu (faza 11) +- Snapshoty JSON (seller, buyer, items) gotowe do renderowania HTML/PDF +- receipt_number unikalne — gotowe do wyswietlania + +**Concerns:** +- Brak + +**Blockers:** +- Brak + +--- +*Phase: 10-receipt-issue, Plan: 01* +*Completed: 2026-03-15* diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 79b9315..5063043 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -9,6 +9,7 @@ - `App\Modules\Orders` - `App\Modules\Users` - `App\Modules\Settings` +- `App\Modules\Accounting` (modul paragonow — wystawianie z zamowien) ## Routing - `GET /login`, `POST /login`, `POST /logout` @@ -54,6 +55,10 @@ - `POST /settings/integrations/shoppro/statuses/save` - `POST /settings/integrations/shoppro/statuses/sync` - `POST /settings/integrations/shoppro/delivery/save` +- `GET /settings/accounting` +- `POST /settings/accounting/save` +- `POST /settings/accounting/toggle` +- `POST /settings/accounting/delete` - `GET /health` - `GET /` (redirect) @@ -111,6 +116,10 @@ - `App\Modules\Settings\AllegroOrdersSyncService` - `App\Modules\Settings\AllegroOrderSyncStateRepository` - `App\Modules\Settings\AllegroStatusSyncService` +- `App\Modules\Settings\ReceiptConfigController` +- `App\Modules\Settings\ReceiptConfigRepository` +- `App\Modules\Accounting\ReceiptRepository` +- `App\Modules\Accounting\ReceiptController` - `App\Modules\Shipments\ShipmentProviderInterface` - `App\Modules\Shipments\ShipmentProviderRegistry` - `App\Modules\Shipments\ApaczkaShipmentService` @@ -197,6 +206,39 @@ - `Statusy` (`/settings/statuses`). - `Cron` (`/settings/cron`). - `Integracje` (`/settings/integrations`) - wspolny hub konfiguracji providerow. + - `Ksiegowosc` (`/settings/accounting`) - konfiguracja paragonow. + +## Przeplyw Ustawienia > Ksiegowosc (konfiguracja paragonow) +- `GET /settings/accounting`: + - `ReceiptConfigController::index(Request): Response` + - pobiera liste konfiguracji przez `ReceiptConfigRepository::listAll()`, + - opcjonalnie laduje konfiguracje do edycji przez `findById()` (query param `edit`), + - renderuje widok `resources/views/settings/accounting.php`. +- `POST /settings/accounting/save`: + - `ReceiptConfigController::save(Request): Response` + - waliduje CSRF, nazwe (wymagana) i format numeracji (wymagany, musi zawierac `%N`), + - zapisuje przez `ReceiptConfigRepository::save(...)` (INSERT lub UPDATE wg obecnosci `id`). +- `POST /settings/accounting/toggle`: + - `ReceiptConfigController::toggleStatus(Request): Response` + - przelacza `is_active` przez `ReceiptConfigRepository::toggleStatus(...)`. +- `POST /settings/accounting/delete`: + - `ReceiptConfigController::delete(Request): Response` + - usuwa konfiguracje przez `ReceiptConfigRepository::delete(...)`, + - FK RESTRICT blokuje usuniecie jesli istnieja powiazane paragony. + +## Przeplyw Wystawianie paragonu z zamowienia +- `GET /orders/{id}/receipt/create`: + - `ReceiptController::create(Request): Response` + - pobiera zamowienie (OrdersRepository::findDetails), aktywne konfiguracje, dane sprzedawcy, + - renderuje formularz `resources/views/orders/receipt-create.php`. +- `POST /orders/{id}/receipt/store`: + - `ReceiptController::store(Request): Response` + - waliduje CSRF, config_id, istnienie zamowienia, + - buduje snapshoty: seller_data_json (z company_settings), buyer_data_json (z adresow zamowienia), items_json (z pozycji), + - oblicza total_gross, sale_date (wg sale_date_source z konfiguracji), order_reference_value, + - generuje numer atomowo przez `ReceiptRepository::getNextNumber(...)` (INSERT ON DUPLICATE KEY UPDATE na receipt_number_counters), + - zapisuje paragon przez `ReceiptRepository::create(...)`, + - redirect na /orders/{id} z flash success. ## Przeplyw Ustawienia > Cron - `GET /settings/cron`: diff --git a/database/migrations/20260315_000050_create_receipt_configs_table.sql b/database/migrations/20260315_000050_create_receipt_configs_table.sql new file mode 100644 index 0000000..936c98c --- /dev/null +++ b/database/migrations/20260315_000050_create_receipt_configs_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `receipt_configs` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(128) NOT NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `number_format` VARCHAR(64) NOT NULL DEFAULT 'PAR/%N/%M/%Y', + `numbering_type` ENUM('monthly','yearly') NOT NULL DEFAULT 'monthly', + `is_named` TINYINT(1) NOT NULL DEFAULT 0, + `sale_date_source` ENUM('order_date','payment_date','issue_date') NOT NULL DEFAULT 'issue_date', + `order_reference` ENUM('none','orderpro','integration') NOT NULL DEFAULT 'none', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/20260315_000051_create_receipts_table.sql b/database/migrations/20260315_000051_create_receipts_table.sql new file mode 100644 index 0000000..446da1c --- /dev/null +++ b/database/migrations/20260315_000051_create_receipts_table.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS `receipts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `order_id` BIGINT UNSIGNED NOT NULL, + `config_id` INT UNSIGNED NOT NULL, + `receipt_number` VARCHAR(64) NOT NULL, + `issue_date` DATE NOT NULL, + `sale_date` DATE NOT NULL, + `seller_data_json` JSON NOT NULL, + `buyer_data_json` JSON DEFAULT NULL, + `items_json` JSON NOT NULL, + `total_net` DECIMAL(12,2) NOT NULL DEFAULT 0.00, + `total_gross` DECIMAL(12,2) NOT NULL DEFAULT 0.00, + `order_reference_value` VARCHAR(128) DEFAULT NULL, + `created_by` INT UNSIGNED DEFAULT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `receipts_number_unique` (`receipt_number`), + KEY `receipts_order_idx` (`order_id`), + KEY `receipts_config_idx` (`config_id`), + KEY `receipts_issue_date_idx` (`issue_date`), + CONSTRAINT `receipts_order_fk` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `receipts_config_fk` FOREIGN KEY (`config_id`) REFERENCES `receipt_configs` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/20260315_000052_create_receipt_number_counters_table.sql b/database/migrations/20260315_000052_create_receipt_number_counters_table.sql new file mode 100644 index 0000000..95a4853 --- /dev/null +++ b/database/migrations/20260315_000052_create_receipt_number_counters_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS `receipt_number_counters` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `config_id` INT UNSIGNED NOT NULL, + `year` SMALLINT UNSIGNED NOT NULL, + `month` TINYINT UNSIGNED DEFAULT NULL, + `last_number` INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `receipt_counters_config_period_unique` (`config_id`, `year`, `month`), + CONSTRAINT `receipt_counters_config_fk` FOREIGN KEY (`config_id`) REFERENCES `receipt_configs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/20260315_000053_extend_company_settings_extra_fields.sql b/database/migrations/20260315_000053_extend_company_settings_extra_fields.sql new file mode 100644 index 0000000..838713e --- /dev/null +++ b/database/migrations/20260315_000053_extend_company_settings_extra_fields.sql @@ -0,0 +1,5 @@ +ALTER TABLE `company_settings` + ADD COLUMN IF NOT EXISTS `bdo_number` VARCHAR(20) DEFAULT NULL AFTER `bank_owner_name`, + ADD COLUMN IF NOT EXISTS `regon` VARCHAR(14) DEFAULT NULL AFTER `bdo_number`, + ADD COLUMN IF NOT EXISTS `court_register` VARCHAR(128) DEFAULT NULL AFTER `regon`, + ADD COLUMN IF NOT EXISTS `logo_path` VARCHAR(255) DEFAULT NULL AFTER `court_register`; diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 9bceac3..7a2ec3f 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -69,6 +69,12 @@ width: 100%; } +.btn--disabled { + opacity: 0.3; + cursor: not-allowed; + pointer-events: none; +} + .btn:active { transform: translateY(1px); } @@ -81,10 +87,10 @@ .form-control { width: 100%; - min-height: 34px; + min-height: 30px; border: 1px solid var(--c-border); - border-radius: 8px; - padding: 5px 10px; + border-radius: 6px; + padding: 4px 8px; font: inherit; color: var(--c-text-strong); background: #ffffff; @@ -674,7 +680,7 @@ h4.section-title::before { .filters-actions { display: flex; - align-items: end; + align-items: center; gap: 8px; } @@ -692,18 +698,21 @@ h4.section-title::before { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-actions { @@ -1017,6 +1026,7 @@ h4.section-title::before { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + align-items: end; } .table-col-toggle-wrapper { diff --git a/resources/lang/pl.php b/resources/lang/pl.php index c670fae..667782f 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -33,6 +33,7 @@ return [ 'apaczka' => 'Integracja Apaczka', 'inpost' => 'Integracja InPost', 'company' => 'Dane firmy', + 'accounting' => 'Ksiegowosc', ], 'marketplace' => [ 'title' => 'Marketplace', @@ -188,6 +189,7 @@ return [ 'shipment_created' => 'Przesylka WZA', 'shipment_label_downloaded' => 'Etykieta pobrana', 'shipment_error' => 'Blad przesylki', + 'receipt_issued' => 'Paragon wystawiony', ], 'actors' => [ 'system' => 'System', @@ -1124,6 +1126,76 @@ return [ 'save_failed' => 'Nie udalo sie zapisac danych firmy.', ], ], + 'accounting' => [ + 'title' => 'Konfiguracje paragonow', + 'description' => 'Zarzadzaj szablonami numeracji i ustawieniami paragonow.', + 'table' => [ + 'heading' => 'Lista konfiguracji', + 'empty' => 'Brak konfiguracji paragonow. Dodaj pierwsza ponizej.', + 'name' => 'Nazwa', + 'number_format' => 'Format numeracji', + 'numbering_type' => 'Typ numeracji', + 'is_named' => 'Imienny', + 'sale_date_source' => 'Data sprzedazy', + 'order_reference' => 'Ref. zamowienia', + 'status' => 'Status', + 'actions' => 'Akcje', + ], + 'form' => [ + 'add_heading' => 'Dodaj konfiguracje', + 'edit_heading' => 'Edytuj konfiguracje', + ], + 'fields' => [ + 'name' => 'Nazwa konfiguracji', + 'number_format' => 'Format numeracji', + 'number_format_hint' => 'Zmienne: %N — numer, %M — miesiac, %Y — rok', + 'numbering_type' => 'Typ numeracji', + 'is_named' => 'Paragon imienny (dane klienta)', + 'is_active' => 'Aktywna', + 'sale_date_source' => 'Domyslna data sprzedazy', + 'order_reference' => 'Informacja o zamowieniu', + ], + 'options' => [ + 'yes' => 'Tak', + 'no' => 'Nie', + 'active' => 'Aktywna', + 'inactive' => 'Nieaktywna', + 'numbering_type' => [ + 'monthly' => 'Miesieczny', + 'yearly' => 'Roczny', + ], + 'sale_date_source' => [ + 'order_date' => 'Wg daty zamowienia', + 'payment_date' => 'Wg daty oplacenia', + 'issue_date' => 'Wg daty wystawienia', + ], + 'order_reference' => [ + 'none' => 'Brak', + 'orderpro' => 'Numer orderPRO', + 'integration' => 'Numer z integracji', + ], + ], + 'actions' => [ + 'edit' => 'Edytuj', + 'delete' => 'Usun', + 'activate' => 'Aktywuj', + 'deactivate' => 'Dezaktywuj', + 'save_new' => 'Dodaj konfiguracje', + 'save_edit' => 'Zapisz zmiany', + 'cancel' => 'Anuluj', + ], + 'confirm' => [ + 'delete_title' => 'Usunac konfiguracje?', + 'delete_message' => 'Czy na pewno chcesz usunac te konfiguracje paragonow?', + ], + 'flash' => [ + 'saved' => 'Konfiguracja zapisana.', + 'save_failed' => 'Blad zapisu konfiguracji.', + 'deleted' => 'Konfiguracja usunieta.', + 'delete_failed' => 'Nie mozna usunac — konfiguracja ma powiazane paragony.', + 'toggled' => 'Status konfiguracji zmieniony.', + ], + ], 'products' => [ 'title' => 'Produkty', 'description' => 'Ustawienia generatora SKU dla produktow.', @@ -1147,4 +1219,29 @@ return [ 'title' => 'Przygotuj przesylke', ], ], + 'receipts' => [ + 'create' => [ + 'title' => 'Wystaw paragon', + 'back' => 'Powrot do zamowienia', + 'select_config' => 'Konfiguracja paragonu', + 'issue_date' => 'Data wystawienia', + 'items' => 'Pozycje zamowienia', + 'total' => 'Razem brutto', + 'seller_data' => 'Dane sprzedawcy', + 'submit' => 'Wystaw paragon', + 'cancel' => 'Anuluj', + 'no_configs' => 'Brak aktywnych konfiguracji paragonow', + 'no_config_selected' => 'Wybierz konfiguracje paragonu', + 'invalid_config' => 'Nieprawidlowa konfiguracja paragonu', + ], + 'documents' => [ + 'number' => 'Numer', + 'issue_date' => 'Data wystawienia', + 'total_gross' => 'Kwota brutto', + 'config' => 'Konfiguracja', + 'actions' => 'Akcje', + 'preview' => 'Podglad', + 'empty' => 'Brak dokumentow', + ], + ], ]; diff --git a/resources/scss/app.scss b/resources/scss/app.scss index e6ffac6..9b2f7ff 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -441,7 +441,7 @@ h4.section-title { .filters-actions { display: flex; - align-items: end; + align-items: center; gap: 8px; } @@ -459,18 +459,21 @@ h4.section-title { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; + align-items: start; } .form-actions { @@ -784,6 +787,7 @@ h4.section-title { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + align-items: end; } .table-col-toggle-wrapper { diff --git a/resources/scss/shared/_ui-components.scss b/resources/scss/shared/_ui-components.scss index 61f3a36..dfdb111 100644 --- a/resources/scss/shared/_ui-components.scss +++ b/resources/scss/shared/_ui-components.scss @@ -68,6 +68,12 @@ width: 100%; } +.btn--disabled { + opacity: 0.3; + cursor: not-allowed; + pointer-events: none; +} + .btn:active { transform: translateY(1px); } @@ -80,10 +86,10 @@ .form-control { width: 100%; - min-height: 34px; + min-height: 30px; border: 1px solid var(--c-border); - border-radius: 8px; - padding: 5px 10px; + border-radius: 6px; + padding: 4px 8px; font: inherit; color: var(--c-text-strong); background: #ffffff; diff --git a/resources/views/components/table-list.php b/resources/views/components/table-list.php index 1ffaf46..9b343f6 100644 --- a/resources/views/components/table-list.php +++ b/resources/views/components/table-list.php @@ -156,10 +156,13 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string -
- - Wyczysc -
+ diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index 24d0ec2..a5d3e47 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -79,6 +79,9 @@ + + + diff --git a/resources/views/orders/receipt-create.php b/resources/views/orders/receipt-create.php new file mode 100644 index 0000000..c75e9d5 --- /dev/null +++ b/resources/views/orders/receipt-create.php @@ -0,0 +1,104 @@ + + +
+
+
+ +

+
+ +
+
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ +

+
+ + + + + + + + + + + + + + + + $item): ?> + + + + + + + + + + + + + + + + + +
Lp.SKU/EAN
Brak pozycji
+
+
+
+
+ +

+
+
+
Firma
+
NIP
+
Adres
,
+
Telefon
+
Email
+
+
+ +
+ + +
+
+
diff --git a/resources/views/orders/show.php b/resources/views/orders/show.php index 3da5b3d..ace78f0 100644 --- a/resources/views/orders/show.php +++ b/resources/views/orders/show.php @@ -7,6 +7,8 @@ $shipmentsList = is_array($shipments ?? null) ? $shipments : []; $packagesList = is_array($packages ?? null) ? $packages : []; $documentsList = is_array($documents ?? null) ? $documents : []; $notesList = is_array($notes ?? null) ? $notes : []; +$receiptsList = is_array($receipts ?? null) ? $receipts : []; +$receiptConfigsList = is_array($receiptConfigs ?? null) ? $receiptConfigs : []; $historyList = is_array($history ?? null) ? $history : []; $activityLogList = is_array($activityLog ?? null) ? $activityLog : []; $statusPanelList = is_array($statusPanel ?? null) ? $statusPanel : []; @@ -45,12 +47,15 @@ foreach ($addressesList as $address) {
- + Przygotuj przesylke - - - - + + Wystaw paragon + + + + +
@@ -100,7 +105,7 @@ foreach ($addressesList as $address) { - +
@@ -516,7 +521,63 @@ foreach ($addressesList as $address) {

-
+ +

+ + +

Paragony

+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +

Dokumenty zewnetrzne

+
+ + + + + + + + + + + + + + + + + + + +
NumerTypKwota bruttoData
+
+
diff --git a/resources/views/settings/accounting.php b/resources/views/settings/accounting.php new file mode 100644 index 0000000..7bba3e5 --- /dev/null +++ b/resources/views/settings/accounting.php @@ -0,0 +1,158 @@ + + +
+

+

+ + + + + +
+ +
+ +
+

+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+
+ + + +
+
+
+ +
+ +
+

+ +
+ + + + + +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + + +
+
+
+ + diff --git a/resources/views/settings/company.php b/resources/views/settings/company.php index 4e97b80..7c518c0 100644 --- a/resources/views/settings/company.php +++ b/resources/views/settings/company.php @@ -72,6 +72,26 @@ $s = is_array($settings ?? null) ? $settings : []; +
+ + + +
+ + +

diff --git a/routes/web.php b/routes/web.php index 5d60336..087a21d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,6 +34,10 @@ use App\Modules\Settings\ShopproIntegrationsRepository; use App\Modules\Settings\ShopproStatusMappingRepository; use App\Modules\Settings\CompanySettingsController; use App\Modules\Settings\CompanySettingsRepository; +use App\Modules\Settings\ReceiptConfigController; +use App\Modules\Settings\ReceiptConfigRepository; +use App\Modules\Accounting\ReceiptController; +use App\Modules\Accounting\ReceiptRepository; use App\Modules\Settings\CronSettingsController; use App\Modules\Settings\SettingsController; use App\Modules\Shipments\ApaczkaShipmentService; @@ -53,7 +57,9 @@ return static function (Application $app): void { $authController = new AuthController($template, $auth, $translator); $usersController = new UsersController($template, $translator, $auth, $app->users()); $shipmentPackageRepositoryForOrders = new ShipmentPackageRepository($app->db()); - $ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders); + $receiptConfigRepository = new ReceiptConfigRepository($app->db()); + $receiptRepository = new ReceiptRepository($app->db()); + $ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository); $settingsController = new SettingsController($template, $translator, $auth, $app->migrator(), $app->orderStatuses()); $allegroIntegrationRepository = new AllegroIntegrationRepository( $app->db(), @@ -171,6 +177,21 @@ return static function (Application $app): void { $auth, $companySettingsRepository ); + $receiptConfigController = new ReceiptConfigController( + $template, + $translator, + $auth, + $receiptConfigRepository + ); + $receiptController = new ReceiptController( + $template, + $translator, + $auth, + $receiptRepository, + $receiptConfigRepository, + $companySettingsRepository, + new OrdersRepository($app->db()) + ); $allegroApiClient = new AllegroApiClient(); $shipmentPackageRepository = new ShipmentPackageRepository($app->db()); $shipmentService = new AllegroShipmentService( @@ -274,6 +295,12 @@ return static function (Application $app): void { $router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]); $router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]); $router->post('/settings/company/save', [$companySettingsController, 'save'], [$authMiddleware]); + $router->get('/settings/accounting', [$receiptConfigController, 'index'], [$authMiddleware]); + $router->post('/settings/accounting/save', [$receiptConfigController, 'save'], [$authMiddleware]); + $router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]); + $router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]); + $router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]); + $router->post('/orders/{id}/receipt/store', [$receiptController, 'store'], [$authMiddleware]); $router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]); $router->post('/orders/{id}/shipment/create', [$shipmentController, 'create'], [$authMiddleware]); $router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]); diff --git a/src/Modules/Accounting/ReceiptController.php b/src/Modules/Accounting/ReceiptController.php new file mode 100644 index 0000000..e72ced0 --- /dev/null +++ b/src/Modules/Accounting/ReceiptController.php @@ -0,0 +1,263 @@ +input('id', 0)); + $details = $this->orders->findDetails($orderId); + if ($details === null) { + return Response::html('Not found', 404); + } + + $configs = array_filter($this->receiptConfigs->listAll(), static fn(array $c): bool => (int) ($c['is_active'] ?? 0) === 1); + if ($configs === []) { + Flash::set('order.error', $this->translator->get('receipts.create.no_configs')); + return Response::redirect('/orders/' . $orderId); + } + + $order = is_array($details['order'] ?? null) ? $details['order'] : []; + $items = is_array($details['items'] ?? null) ? $details['items'] : []; + $seller = $this->companySettings->getSettings(); + + $totalGross = 0.0; + foreach ($items as $item) { + $qty = (float) ($item['quantity'] ?? 0); + $price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0; + $totalGross += $qty * $price; + } + + $html = $this->template->render('orders/receipt-create', [ + 'title' => $this->translator->get('receipts.create.title'), + 'activeMenu' => 'orders', + 'activeOrders' => 'list', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'orderId' => $orderId, + 'order' => $order, + 'items' => $items, + 'configs' => array_values($configs), + 'seller' => $seller, + 'totalGross' => $totalGross, + ], 'layouts/app'); + + return Response::html($html); + } + + public function store(Request $request): Response + { + $orderId = max(0, (int) $request->input('id', 0)); + + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('order.error', 'Nieprawidlowy token CSRF'); + return Response::redirect('/orders/' . $orderId); + } + + $configId = (int) $request->input('config_id', '0'); + if ($configId <= 0) { + Flash::set('order.error', $this->translator->get('receipts.create.no_config_selected')); + return Response::redirect('/orders/' . $orderId . '/receipt/create'); + } + + $config = $this->receiptConfigs->findById($configId); + if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) { + Flash::set('order.error', $this->translator->get('receipts.create.invalid_config')); + return Response::redirect('/orders/' . $orderId . '/receipt/create'); + } + + $details = $this->orders->findDetails($orderId); + if ($details === null) { + return Response::html('Not found', 404); + } + + $order = is_array($details['order'] ?? null) ? $details['order'] : []; + $items = is_array($details['items'] ?? null) ? $details['items'] : []; + $addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : []; + $payments = is_array($details['payments'] ?? null) ? $details['payments'] : []; + + $seller = $this->companySettings->getSettings(); + $sellerSnapshot = [ + 'company_name' => $seller['company_name'] ?? '', + 'tax_number' => $seller['tax_number'] ?? '', + 'street' => $seller['street'] ?? '', + 'city' => $seller['city'] ?? '', + 'postal_code' => $seller['postal_code'] ?? '', + 'phone' => $seller['phone'] ?? '', + 'email' => $seller['email'] ?? '', + 'bank_account' => $seller['bank_account'] ?? '', + 'bdo_number' => $seller['bdo_number'] ?? '', + 'regon' => $seller['regon'] ?? '', + 'court_register' => $seller['court_register'] ?? '', + ]; + + $buyerAddress = $this->resolveBuyerAddress($addresses); + $buyerSnapshot = $buyerAddress !== null ? [ + 'name' => $buyerAddress['name'] ?? '', + 'company_name' => $buyerAddress['company_name'] ?? '', + 'tax_number' => $buyerAddress['company_tax_number'] ?? '', + 'street' => trim(($buyerAddress['street_name'] ?? '') . ' ' . ($buyerAddress['street_number'] ?? '')), + 'city' => $buyerAddress['city'] ?? '', + 'postal_code' => $buyerAddress['zip_code'] ?? '', + 'phone' => $buyerAddress['phone'] ?? '', + 'email' => $buyerAddress['email'] ?? '', + ] : null; + + $itemsSnapshot = []; + $totalGross = 0.0; + foreach ($items as $item) { + $qty = (float) ($item['quantity'] ?? 0); + $price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0; + $lineTotal = $qty * $price; + $totalGross += $lineTotal; + $itemsSnapshot[] = [ + 'name' => $item['original_name'] ?? '', + 'quantity' => $qty, + 'price' => $price, + 'total' => $lineTotal, + 'sku' => $item['sku'] ?? '', + 'ean' => $item['ean'] ?? '', + ]; + } + + $issueDate = trim((string) $request->input('issue_date', '')); + if ($issueDate === '' || strtotime($issueDate) === false) { + $issueDate = date('Y-m-d'); + } + + $saleDate = $this->resolveSaleDate($config, $order, $payments, $issueDate); + + $orderReference = $this->resolveOrderReference($config, $order); + + try { + $receiptNumber = $this->receipts->getNextNumber( + $configId, + (string) ($config['number_format'] ?? 'PAR/%N/%M/%Y'), + (string) ($config['numbering_type'] ?? 'monthly') + ); + + $user = $this->auth->user(); + + $this->receipts->create([ + 'order_id' => $orderId, + 'config_id' => $configId, + 'receipt_number' => $receiptNumber, + 'issue_date' => $issueDate, + 'sale_date' => $saleDate, + 'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE), + 'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null, + 'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE), + 'total_net' => number_format($totalGross, 2, '.', ''), + 'total_gross' => number_format($totalGross, 2, '.', ''), + 'order_reference_value' => $orderReference, + 'created_by' => is_array($user) ? ($user['id'] ?? null) : null, + ]); + + $userName = is_array($user) ? (string) ($user['username'] ?? $user['email'] ?? '') : ''; + $this->orders->recordActivity( + $orderId, + 'receipt_issued', + 'Wystawiono paragon: ' . $receiptNumber, + ['receipt_number' => $receiptNumber, 'config_id' => $configId, 'total_gross' => number_format($totalGross, 2, '.', '')], + 'user', + $userName !== '' ? $userName : null + ); + + Flash::set('order.success', 'Paragon wystawiony: ' . $receiptNumber); + } catch (Throwable) { + Flash::set('order.error', 'Blad wystawiania paragonu'); + } + + return Response::redirect('/orders/' . $orderId); + } + + /** + * @param list> $addresses + * @return array|null + */ + private function resolveBuyerAddress(array $addresses): ?array + { + $byType = []; + foreach ($addresses as $addr) { + $type = (string) ($addr['address_type'] ?? ''); + if ($type !== '' && !isset($byType[$type])) { + $byType[$type] = $addr; + } + } + + return $byType['invoice'] ?? $byType['customer'] ?? null; + } + + /** + * @param array $config + * @param array $order + * @param list> $payments + */ + private function resolveSaleDate(array $config, array $order, array $payments, string $issueDate): string + { + $source = (string) ($config['sale_date_source'] ?? 'issue_date'); + + if ($source === 'order_date') { + $ordered = (string) ($order['ordered_at'] ?? ''); + if ($ordered !== '') { + $ts = strtotime($ordered); + return $ts !== false ? date('Y-m-d', $ts) : $issueDate; + } + } + + if ($source === 'payment_date' && $payments !== []) { + $lastPayment = $payments[0] ?? []; + $payDate = (string) ($lastPayment['payment_date'] ?? ''); + if ($payDate !== '') { + $ts = strtotime($payDate); + return $ts !== false ? date('Y-m-d', $ts) : $issueDate; + } + } + + return $issueDate; + } + + /** + * @param array $config + * @param array $order + */ + private function resolveOrderReference(array $config, array $order): ?string + { + $ref = (string) ($config['order_reference'] ?? 'none'); + + if ($ref === 'orderpro') { + return (string) ($order['internal_order_number'] ?? ''); + } + + if ($ref === 'integration') { + return (string) ($order['external_order_id'] ?? ''); + } + + return null; + } +} diff --git a/src/Modules/Accounting/ReceiptRepository.php b/src/Modules/Accounting/ReceiptRepository.php new file mode 100644 index 0000000..38850e1 --- /dev/null +++ b/src/Modules/Accounting/ReceiptRepository.php @@ -0,0 +1,121 @@ +> + */ + public function findByOrderId(int $orderId): array + { + $statement = $this->pdo->prepare( + 'SELECT r.*, rc.name AS config_name + FROM receipts r + LEFT JOIN receipt_configs rc ON rc.id = r.config_id + WHERE r.order_id = :order_id + ORDER BY r.created_at DESC' + ); + $statement->execute(['order_id' => $orderId]); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array|null + */ + public function findById(int $id): ?array + { + $statement = $this->pdo->prepare('SELECT * FROM receipts WHERE id = :id LIMIT 1'); + $statement->execute(['id' => $id]); + $row = $statement->fetch(PDO::FETCH_ASSOC); + + return is_array($row) ? $row : null; + } + + /** + * @param array $data + */ + public function create(array $data): int + { + $statement = $this->pdo->prepare( + 'INSERT INTO receipts ( + order_id, config_id, receipt_number, issue_date, sale_date, + seller_data_json, buyer_data_json, items_json, + total_net, total_gross, order_reference_value, created_by + ) VALUES ( + :order_id, :config_id, :receipt_number, :issue_date, :sale_date, + :seller_data_json, :buyer_data_json, :items_json, + :total_net, :total_gross, :order_reference_value, :created_by + )' + ); + + $statement->execute([ + 'order_id' => (int) $data['order_id'], + 'config_id' => (int) $data['config_id'], + 'receipt_number' => (string) $data['receipt_number'], + 'issue_date' => (string) $data['issue_date'], + 'sale_date' => (string) $data['sale_date'], + 'seller_data_json' => (string) $data['seller_data_json'], + 'buyer_data_json' => $data['buyer_data_json'], + 'items_json' => (string) $data['items_json'], + 'total_net' => (string) $data['total_net'], + 'total_gross' => (string) $data['total_gross'], + 'order_reference_value' => $data['order_reference_value'] ?? null, + 'created_by' => $data['created_by'] ?? null, + ]); + + return (int) $this->pdo->lastInsertId(); + } + + public function getNextNumber(int $configId, string $numberFormat, string $numberingType): string + { + $year = (int) date('Y'); + $month = $numberingType === 'yearly' ? null : (int) date('n'); + + if ($month === null) { + $this->pdo->prepare( + 'INSERT INTO receipt_number_counters (config_id, year, month, last_number) + VALUES (:config_id, :year, NULL, 1) + ON DUPLICATE KEY UPDATE last_number = last_number + 1' + )->execute(['config_id' => $configId, 'year' => $year]); + + $stmt = $this->pdo->prepare( + 'SELECT last_number FROM receipt_number_counters + WHERE config_id = :config_id AND year = :year AND month IS NULL' + ); + $stmt->execute(['config_id' => $configId, 'year' => $year]); + } else { + $this->pdo->prepare( + 'INSERT INTO receipt_number_counters (config_id, year, month, last_number) + VALUES (:config_id, :year, :month, 1) + ON DUPLICATE KEY UPDATE last_number = last_number + 1' + )->execute(['config_id' => $configId, 'year' => $year, 'month' => $month]); + + $stmt = $this->pdo->prepare( + 'SELECT last_number FROM receipt_number_counters + WHERE config_id = :config_id AND year = :year AND month = :month' + ); + $stmt->execute(['config_id' => $configId, 'year' => $year, 'month' => $month]); + } + + $lastNumber = (int) $stmt->fetchColumn(); + + $number = str_replace( + ['%N', '%M', '%Y'], + [str_pad((string) $lastNumber, 3, '0', STR_PAD_LEFT), str_pad((string) ($month ?? 1), 2, '0', STR_PAD_LEFT), (string) $year], + $numberFormat + ); + + return $number; + } +} diff --git a/src/Modules/Orders/OrdersController.php b/src/Modules/Orders/OrdersController.php index 99c0175..ef3beb7 100644 --- a/src/Modules/Orders/OrdersController.php +++ b/src/Modules/Orders/OrdersController.php @@ -10,7 +10,9 @@ use App\Core\Security\Csrf; use App\Core\View\Template; use App\Core\Support\Flash; use App\Core\Support\StringHelper; +use App\Modules\Accounting\ReceiptRepository; use App\Modules\Auth\AuthService; +use App\Modules\Settings\ReceiptConfigRepository; use App\Modules\Shipments\ShipmentPackageRepository; final class OrdersController @@ -20,7 +22,9 @@ final class OrdersController private readonly Translator $translator, private readonly AuthService $auth, private readonly OrdersRepository $orders, - private readonly ?ShipmentPackageRepository $shipmentPackages = null + private readonly ?ShipmentPackageRepository $shipmentPackages = null, + private readonly ?ReceiptRepository $receiptRepo = null, + private readonly ?ReceiptConfigRepository $receiptConfigRepo = null ) { } @@ -162,6 +166,18 @@ final class OrdersController ? $this->shipmentPackages->findByOrderId($orderId) : []; + $receipts = $this->receiptRepo !== null + ? $this->receiptRepo->findByOrderId($orderId) + : []; + + $activeReceiptConfigs = []; + if ($this->receiptConfigRepo !== null) { + $activeReceiptConfigs = array_filter( + $this->receiptConfigRepo->listAll(), + static fn(array $c): bool => (int) ($c['is_active'] ?? 0) === 1 + ); + } + $flashSuccess = (string) Flash::get('order.success', ''); $flashError = (string) Flash::get('order.error', ''); @@ -188,6 +204,8 @@ final class OrdersController 'currentStatusCode' => $statusCode, 'flashSuccess' => $flashSuccess, 'flashError' => $flashError, + 'receipts' => $receipts, + 'receiptConfigs' => $activeReceiptConfigs, ], 'layouts/app'); return Response::html($html); diff --git a/src/Modules/Settings/CompanySettingsRepository.php b/src/Modules/Settings/CompanySettingsRepository.php index 0a0306d..47a7f2e 100644 --- a/src/Modules/Settings/CompanySettingsRepository.php +++ b/src/Modules/Settings/CompanySettingsRepository.php @@ -44,6 +44,10 @@ final class CompanySettingsRepository 'tax_number' => trim((string) ($row['tax_number'] ?? '')), 'bank_account' => trim((string) ($row['bank_account'] ?? '')), 'bank_owner_name' => trim((string) ($row['bank_owner_name'] ?? '')), + 'bdo_number' => trim((string) ($row['bdo_number'] ?? '')), + 'regon' => trim((string) ($row['regon'] ?? '')), + 'court_register' => trim((string) ($row['court_register'] ?? '')), + 'logo_path' => trim((string) ($row['logo_path'] ?? '')), 'default_package_length_cm' => (float) ($row['default_package_length_cm'] ?? 25.0), 'default_package_width_cm' => (float) ($row['default_package_width_cm'] ?? 20.0), 'default_package_height_cm' => (float) ($row['default_package_height_cm'] ?? 8.0), @@ -73,6 +77,10 @@ final class CompanySettingsRepository tax_number = :tax_number, bank_account = :bank_account, bank_owner_name = :bank_owner_name, + bdo_number = :bdo_number, + regon = :regon, + court_register = :court_register, + logo_path = :logo_path, default_package_length_cm = :length, default_package_width_cm = :width, default_package_height_cm = :height, @@ -95,6 +103,10 @@ final class CompanySettingsRepository 'tax_number' => StringHelper::nullableString((string) ($data['tax_number'] ?? '')), 'bank_account' => StringHelper::nullableString((string) ($data['bank_account'] ?? '')), 'bank_owner_name' => StringHelper::nullableString((string) ($data['bank_owner_name'] ?? '')), + 'bdo_number' => StringHelper::nullableString((string) ($data['bdo_number'] ?? '')), + 'regon' => StringHelper::nullableString((string) ($data['regon'] ?? '')), + 'court_register' => StringHelper::nullableString((string) ($data['court_register'] ?? '')), + 'logo_path' => StringHelper::nullableString((string) ($data['logo_path'] ?? '')), 'length' => max(0.1, (float) ($data['default_package_length_cm'] ?? 25.0)), 'width' => max(0.1, (float) ($data['default_package_width_cm'] ?? 20.0)), 'height' => max(0.1, (float) ($data['default_package_height_cm'] ?? 8.0)), @@ -151,6 +163,10 @@ final class CompanySettingsRepository 'tax_number' => '', 'bank_account' => '', 'bank_owner_name' => '', + 'bdo_number' => '', + 'regon' => '', + 'court_register' => '', + 'logo_path' => '', 'default_package_length_cm' => 25.0, 'default_package_width_cm' => 20.0, 'default_package_height_cm' => 8.0, diff --git a/src/Modules/Settings/ReceiptConfigController.php b/src/Modules/Settings/ReceiptConfigController.php new file mode 100644 index 0000000..b3d2735 --- /dev/null +++ b/src/Modules/Settings/ReceiptConfigController.php @@ -0,0 +1,136 @@ +translator; + $configs = $this->repository->listAll(); + + $editConfig = null; + $editId = (int) $request->input('edit', '0'); + if ($editId > 0) { + $editConfig = $this->repository->findById($editId); + } + + $html = $this->template->render('settings/accounting', [ + 'title' => $t->get('settings.accounting.title'), + 'activeMenu' => 'settings', + 'activeSettings' => 'accounting', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'configs' => $configs, + 'editConfig' => $editConfig, + 'successMessage' => Flash::get('settings.accounting.success', ''), + 'errorMessage' => Flash::get('settings.accounting.error', ''), + ], 'layouts/app'); + + return Response::html($html); + } + + public function save(Request $request): Response + { + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF'); + return Response::redirect('/settings/accounting'); + } + + $name = trim((string) $request->input('name', '')); + $numberFormat = trim((string) $request->input('number_format', '')); + + if ($name === '') { + Flash::set('settings.accounting.error', 'Nazwa konfiguracji jest wymagana'); + return Response::redirect('/settings/accounting'); + } + + if ($numberFormat === '' || strpos($numberFormat, '%N') === false) { + Flash::set('settings.accounting.error', 'Format numeracji jest wymagany i musi zawierac %N'); + return Response::redirect('/settings/accounting'); + } + + try { + $this->repository->save([ + 'id' => $request->input('id', ''), + 'name' => $name, + 'is_active' => $request->input('is_active', null), + 'number_format' => $numberFormat, + 'numbering_type' => $request->input('numbering_type', 'monthly'), + 'is_named' => $request->input('is_named', null), + 'sale_date_source' => $request->input('sale_date_source', 'issue_date'), + 'order_reference' => $request->input('order_reference', 'none'), + ]); + + Flash::set('settings.accounting.success', $this->translator->get('settings.accounting.flash.saved')); + } catch (Throwable) { + Flash::set('settings.accounting.error', $this->translator->get('settings.accounting.flash.save_failed')); + } + + return Response::redirect('/settings/accounting'); + } + + public function toggleStatus(Request $request): Response + { + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF'); + return Response::redirect('/settings/accounting'); + } + + $id = (int) $request->input('id', '0'); + if ($id <= 0) { + Flash::set('settings.accounting.error', 'Nieprawidlowy identyfikator konfiguracji'); + return Response::redirect('/settings/accounting'); + } + + try { + $this->repository->toggleStatus($id); + Flash::set('settings.accounting.success', $this->translator->get('settings.accounting.flash.toggled')); + } catch (Throwable) { + Flash::set('settings.accounting.error', 'Blad zmiany statusu'); + } + + return Response::redirect('/settings/accounting'); + } + + public function delete(Request $request): Response + { + if (!Csrf::validate((string) $request->input('_token', ''))) { + Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF'); + return Response::redirect('/settings/accounting'); + } + + $id = (int) $request->input('id', '0'); + if ($id <= 0) { + Flash::set('settings.accounting.error', 'Nieprawidlowy identyfikator konfiguracji'); + return Response::redirect('/settings/accounting'); + } + + try { + $this->repository->delete($id); + Flash::set('settings.accounting.success', $this->translator->get('settings.accounting.flash.deleted')); + } catch (Throwable) { + Flash::set('settings.accounting.error', $this->translator->get('settings.accounting.flash.delete_failed')); + } + + return Response::redirect('/settings/accounting'); + } +} diff --git a/src/Modules/Settings/ReceiptConfigRepository.php b/src/Modules/Settings/ReceiptConfigRepository.php new file mode 100644 index 0000000..cc36f3b --- /dev/null +++ b/src/Modules/Settings/ReceiptConfigRepository.php @@ -0,0 +1,99 @@ +> + */ + public function listAll(): array + { + $statement = $this->pdo->prepare('SELECT * FROM receipt_configs ORDER BY created_at DESC'); + $statement->execute(); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array|null + */ + public function findById(int $id): ?array + { + $statement = $this->pdo->prepare('SELECT * FROM receipt_configs WHERE id = :id LIMIT 1'); + $statement->execute(['id' => $id]); + $row = $statement->fetch(PDO::FETCH_ASSOC); + + return is_array($row) ? $row : null; + } + + /** + * @param array $data + */ + public function save(array $data): void + { + $id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null; + + $params = [ + 'name' => trim((string) ($data['name'] ?? '')), + 'is_active' => isset($data['is_active']) ? 1 : 0, + 'number_format' => trim((string) ($data['number_format'] ?? 'PAR/%N/%M/%Y')), + 'numbering_type' => in_array((string) ($data['numbering_type'] ?? ''), ['monthly', 'yearly'], true) + ? (string) $data['numbering_type'] + : 'monthly', + 'is_named' => isset($data['is_named']) ? 1 : 0, + 'sale_date_source' => in_array((string) ($data['sale_date_source'] ?? ''), ['order_date', 'payment_date', 'issue_date'], true) + ? (string) $data['sale_date_source'] + : 'issue_date', + 'order_reference' => in_array((string) ($data['order_reference'] ?? ''), ['none', 'orderpro', 'integration'], true) + ? (string) $data['order_reference'] + : 'none', + ]; + + if ($id !== null) { + $statement = $this->pdo->prepare( + 'UPDATE receipt_configs SET + name = :name, + is_active = :is_active, + number_format = :number_format, + numbering_type = :numbering_type, + is_named = :is_named, + sale_date_source = :sale_date_source, + order_reference = :order_reference + WHERE id = :id' + ); + $params['id'] = $id; + } else { + $statement = $this->pdo->prepare( + 'INSERT INTO receipt_configs (name, is_active, number_format, numbering_type, is_named, sale_date_source, order_reference) + VALUES (:name, :is_active, :number_format, :numbering_type, :is_named, :sale_date_source, :order_reference)' + ); + } + + $statement->execute($params); + } + + public function toggleStatus(int $id): void + { + $statement = $this->pdo->prepare( + 'UPDATE receipt_configs SET is_active = NOT is_active WHERE id = :id' + ); + $statement->execute(['id' => $id]); + } + + public function delete(int $id): void + { + $statement = $this->pdo->prepare('DELETE FROM receipt_configs WHERE id = :id'); + $statement->execute(['id' => $id]); + } +}