feat(08-10-receipt-module): phases 08-10 complete — receipt issuing from orders
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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*
|
||||
|
||||
203
.paul/phases/08-db-foundation/08-01-PLAN.md
Normal file
203
.paul/phases/08-db-foundation/08-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## 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.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Weryfikacja i wdrożenie migracji SQL</name>
|
||||
<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
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>Sprawdzić SHOW CREATE TABLE dla receipt_configs, receipts, receipt_number_counters; SHOW COLUMNS FROM company_settings</verify>
|
||||
<done>AC-1 i AC-2 spełnione: tabele istnieją z poprawnymi kolumnami i kluczami</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rozszerzenie formularza danych firmy o nowe pola</name>
|
||||
<files>
|
||||
src/Modules/Settings/CompanySettingsRepository.php,
|
||||
resources/views/settings/company.php
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>Odwiedzić /settings/company — zobaczyć nowe pola, wypełnić, zapisać, odświeżyć — dane zachowane</verify>
|
||||
<done>AC-3 spełnione: nowe pola widoczne i zapisywalne</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Migracje DB dla modułu paragonów + rozszerzony formularz danych firmy</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Aktualizacja dokumentacji DB_SCHEMA.md i ARCHITECTURE.md</name>
|
||||
<files>DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>Przejrzeć oba pliki — sprawdzić kompletność opisów nowych tabel</verify>
|
||||
<done>AC-4 spełnione: dokumentacja odzwierciedla nowy schemat</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/08-db-foundation/08-01-SUMMARY.md`
|
||||
</output>
|
||||
131
.paul/phases/08-db-foundation/08-01-SUMMARY.md
Normal file
131
.paul/phases/08-db-foundation/08-01-SUMMARY.md
Normal file
@@ -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*
|
||||
308
.paul/phases/09-receipt-config/09-01-PLAN.md
Normal file
308
.paul/phases/09-receipt-config/09-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: ReceiptConfigRepository — CRUD na receipt_configs</name>
|
||||
<files>src/Modules/Settings/ReceiptConfigRepository.php</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>Sprawdzic syntax PHP: php -l src/Modules/Settings/ReceiptConfigRepository.php</verify>
|
||||
<done>AC-2, AC-3, AC-4, AC-5 (backend) spelnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: ReceiptConfigController — kontroler CRUD</name>
|
||||
<files>src/Modules/Settings/ReceiptConfigController.php</files>
|
||||
<action>
|
||||
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'))
|
||||
</action>
|
||||
<verify>Sprawdzic syntax PHP: php -l src/Modules/Settings/ReceiptConfigController.php</verify>
|
||||
<done>AC-1, AC-2, AC-3, AC-4, AC-5 (kontroler) spelnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Widok settings/accounting.php, routing, nawigacja, tlumaczenia</name>
|
||||
<files>
|
||||
resources/views/settings/accounting.php,
|
||||
resources/views/layouts/app.php,
|
||||
routes/web.php,
|
||||
resources/lang/pl.php
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'accounting' ? ' is-active' : '' ?>" href="/settings/accounting">
|
||||
<?= $e($t('navigation.accounting')) ?>
|
||||
</a>
|
||||
```
|
||||
|
||||
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)
|
||||
</action>
|
||||
<verify>
|
||||
Sprawdzic syntax: php -l resources/views/settings/accounting.php
|
||||
Sprawdzic syntax routes: php -l routes/web.php
|
||||
</verify>
|
||||
<done>AC-1, AC-6 spelnione</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>CRUD konfiguracji paragonow w Ustawienia > Ksiegowosc</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Aktualizacja ARCHITECTURE.md</name>
|
||||
<files>DOCS/ARCHITECTURE.md</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>Przejrzec ARCHITECTURE.md pod katem kompletnosci</verify>
|
||||
<done>AC-1 do AC-6 udokumentowane</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Uzytkownik moze tworzyc, edytowac, aktywowac/dezaktywowac i usuwac konfiguracje paragonow
|
||||
- Nawigacja w sidebarze dziala poprawnie
|
||||
- Brak regresji w istniejacych ustawieniach
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/09-receipt-config/09-01-SUMMARY.md`
|
||||
</output>
|
||||
126
.paul/phases/09-receipt-config/09-01-SUMMARY.md
Normal file
126
.paul/phases/09-receipt-config/09-01-SUMMARY.md
Normal file
@@ -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*
|
||||
320
.paul/phases/10-receipt-issue/10-01-PLAN.md
Normal file
320
.paul/phases/10-receipt-issue/10-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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"
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## 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
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: ReceiptRepository — CRUD + atomowe numerowanie</name>
|
||||
<files>src/Modules/Accounting/ReceiptRepository.php</files>
|
||||
<action>
|
||||
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`
|
||||
</action>
|
||||
<verify>Klasa parsuje sie bez bledow: `php -l src/Modules/Accounting/ReceiptRepository.php`</verify>
|
||||
<done>AC-3 backend spelnione: atomowe numerowanie i zapis paragonu</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: ReceiptController + routing + widok formularza</name>
|
||||
<files>
|
||||
src/Modules/Accounting/ReceiptController.php,
|
||||
resources/views/orders/receipt-create.php,
|
||||
routes/web.php,
|
||||
resources/lang/pl.php
|
||||
</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
- `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
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-5 spelnione: formularz, zapis, walidacja</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Integracja z widokiem zamowienia — przycisk + zakladka Dokumenty</name>
|
||||
<files>
|
||||
src/Modules/Orders/OrdersController.php,
|
||||
resources/views/orders/show.php,
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
<?php if (($receiptConfigs ?? []) !== []): ?>
|
||||
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/create" class="btn btn--secondary">Wystaw paragon</a>
|
||||
<?php endif; ?>
|
||||
```
|
||||
- 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/*
|
||||
</action>
|
||||
<verify>
|
||||
- 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
|
||||
</verify>
|
||||
<done>AC-1, AC-4 spelnione: przycisk w widoku zamowienia + lista paragonow w Dokumentach</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Wystawianie paragonow z poziomu zamowienia — pelny flow: przycisk → formularz → zapis → lista w Dokumentach</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/10-receipt-issue/10-01-SUMMARY.md`
|
||||
</output>
|
||||
142
.paul/phases/10-receipt-issue/10-01-SUMMARY.md
Normal file
142
.paul/phases/10-receipt-issue/10-01-SUMMARY.md
Normal file
@@ -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*
|
||||
@@ -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`:
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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`;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -156,10 +156,13 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="filters-actions">
|
||||
<button type="submit" class="btn btn--primary">Szukaj</button>
|
||||
<a href="<?= $e($basePath) ?>" class="btn btn--secondary js-table-filters-clear">Wyczysc</a>
|
||||
</div>
|
||||
<label class="form-field">
|
||||
<span class="field-label"> </span>
|
||||
<div class="filters-actions">
|
||||
<button type="submit" class="btn btn--primary">Szukaj</button>
|
||||
<a href="<?= $e($basePath) ?>" class="btn btn--secondary js-table-filters-clear">Wyczysc</a>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'company' ? ' is-active' : '' ?>" href="/settings/company">
|
||||
<?= $e($t('navigation.company')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'accounting' ? ' is-active' : '' ?>" href="/settings/accounting">
|
||||
<?= $e($t('navigation.accounting')) ?>
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
104
resources/views/orders/receipt-create.php
Normal file
104
resources/views/orders/receipt-create.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
$orderRow = is_array($order ?? null) ? $order : [];
|
||||
$itemsList = is_array($items ?? null) ? $items : [];
|
||||
$configsList = is_array($configs ?? null) ? $configs : [];
|
||||
$sellerData = is_array($seller ?? null) ? $seller : [];
|
||||
$totalGrossVal = (float) ($totalGross ?? 0);
|
||||
$orderIdVal = (int) ($orderId ?? 0);
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<div class="order-details-head">
|
||||
<div>
|
||||
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">← <?= $e($t('receipts.create.back')) ?></a>
|
||||
<h2 class="section-title mt-12"><?= $e($t('receipts.create.title')) ?></h2>
|
||||
<div class="order-details-sub mt-4">
|
||||
<?= $e($t('orders.details.title')) ?> <?= $e((string) ($orderRow['internal_order_number'] ?? ('#' . $orderIdVal))) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/orders/<?= $e((string) $orderIdVal) ?>/receipt/store" class="mt-16">
|
||||
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
|
||||
|
||||
<div class="form-grid-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="config_id"><?= $e($t('receipts.create.select_config')) ?></label>
|
||||
<select name="config_id" id="config_id" class="form-control" required>
|
||||
<option value="">— <?= $e($t('receipts.create.select_config')) ?> —</option>
|
||||
<?php foreach ($configsList as $cfg): ?>
|
||||
<option value="<?= $e((string) ($cfg['id'] ?? '')) ?>">
|
||||
<?= $e((string) ($cfg['name'] ?? '')) ?> (<?= $e((string) ($cfg['number_format'] ?? '')) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="issue_date"><?= $e($t('receipts.create.issue_date')) ?></label>
|
||||
<input type="date" name="issue_date" id="issue_date" class="form-control" value="<?= $e(date('Y-m-d')) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title mt-16"><?= $e($t('receipts.create.items')) ?></h3>
|
||||
<div class="table-wrap mt-8">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lp.</th>
|
||||
<th><?= $e($t('orders.details.item_name')) ?></th>
|
||||
<th>SKU/EAN</th>
|
||||
<th><?= $e($t('orders.details.item_qty')) ?></th>
|
||||
<th><?= $e($t('orders.details.item_price')) ?></th>
|
||||
<th><?= $e($t('orders.details.item_sum')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($itemsList === []): ?>
|
||||
<tr><td colspan="6" class="muted">Brak pozycji</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($itemsList as $idx => $item): ?>
|
||||
<?php
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : null;
|
||||
$sum = $price !== null ? ($qty * $price) : null;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($idx + 1)) ?></td>
|
||||
<td><?= $e((string) ($item['original_name'] ?? '')) ?></td>
|
||||
<td>
|
||||
<div><?= $e((string) ($item['sku'] ?? '-')) ?></div>
|
||||
<div class="muted"><?= $e((string) ($item['ean'] ?? '-')) ?></div>
|
||||
</td>
|
||||
<td><?= $e((string) $qty) ?></td>
|
||||
<td><?= $e($price !== null ? number_format($price, 2, '.', ' ') : '-') ?></td>
|
||||
<td><?= $e($sum !== null ? number_format($sum, 2, '.', ' ') : '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5" class="text-right"><strong><?= $e($t('receipts.create.total')) ?></strong></td>
|
||||
<td><strong><?= $e(number_format($totalGrossVal, 2, '.', ' ')) ?> <?= $e((string) ($orderRow['currency'] ?? 'PLN')) ?></strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title mt-16"><?= $e($t('receipts.create.seller_data')) ?></h3>
|
||||
<div class="receipt-seller-preview mt-8">
|
||||
<dl class="order-kv">
|
||||
<dt>Firma</dt><dd><?= $e((string) ($sellerData['company_name'] ?? '-')) ?></dd>
|
||||
<dt>NIP</dt><dd><?= $e((string) ($sellerData['tax_number'] ?? '-')) ?></dd>
|
||||
<dt>Adres</dt><dd><?= $e((string) ($sellerData['street'] ?? '')) ?>, <?= $e((string) ($sellerData['postal_code'] ?? '')) ?> <?= $e((string) ($sellerData['city'] ?? '')) ?></dd>
|
||||
<dt>Telefon</dt><dd><?= $e((string) ($sellerData['phone'] ?? '-')) ?></dd>
|
||||
<dt>Email</dt><dd><?= $e((string) ($sellerData['email'] ?? '-')) ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $e($t('receipts.create.submit')) ?></button>
|
||||
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary ml-8"><?= $e($t('receipts.create.cancel')) ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -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) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-details-actions">
|
||||
<button type="button" class="btn btn--secondary">Strefa klienta</button>
|
||||
<button type="button" class="btn btn--secondary btn--disabled">Strefa klienta</button>
|
||||
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--secondary">Przygotuj przesylke</a>
|
||||
<button type="button" class="btn btn--secondary">Platnosc</button>
|
||||
<button type="button" class="btn btn--secondary">Drukuj</button>
|
||||
<button type="button" class="btn btn--primary">Pakuj</button>
|
||||
<button type="button" class="btn btn--secondary">Edytuj</button>
|
||||
<?php if ($receiptConfigsList !== []): ?>
|
||||
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/create" class="btn btn--secondary">Wystaw paragon</a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn--secondary btn--disabled">Platnosc</button>
|
||||
<button type="button" class="btn btn--secondary btn--disabled">Drukuj</button>
|
||||
<button type="button" class="btn btn--primary btn--disabled">Pakuj</button>
|
||||
<button type="button" class="btn btn--secondary btn--disabled">Edytuj</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($flashSuccessMsg !== ''): ?>
|
||||
@@ -100,7 +105,7 @@ foreach ($addressesList as $address) {
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="history"><?= $e($t('orders.details.tabs.history')) ?> (<?= $e((string) count($activityLogList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="shipments"><?= $e($t('orders.details.tabs.shipments')) ?> (<?= $e((string) (count($shipmentsList) + count($packagesList))) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="payments"><?= $e($t('orders.details.tabs.payments')) ?> (<?= $e((string) count($paymentsList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="documents"><?= $e($t('orders.details.tabs.documents')) ?> (<?= $e((string) count($documentsList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="documents"><?= $e($t('orders.details.tabs.documents')) ?> (<?= $e((string) (count($documentsList) + count($receiptsList))) ?>)</button>
|
||||
</section>
|
||||
|
||||
<div class="order-tab-panel is-active" data-order-tab-panel="details">
|
||||
@@ -516,7 +521,63 @@ foreach ($addressesList as $address) {
|
||||
<div class="order-tab-panel" data-order-tab-panel="documents">
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('orders.details.tabs.documents')) ?></h3>
|
||||
<div class="order-empty-placeholder mt-12"></div>
|
||||
<?php if ($receiptsList === [] && $documentsList === []): ?>
|
||||
<p class="muted mt-12"><?= $e($t('receipts.documents.empty')) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($receiptsList !== []): ?>
|
||||
<h4 class="section-title mt-12">Paragony</h4>
|
||||
<div class="table-wrap mt-8">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('receipts.documents.number')) ?></th>
|
||||
<th><?= $e($t('receipts.documents.issue_date')) ?></th>
|
||||
<th><?= $e($t('receipts.documents.total_gross')) ?></th>
|
||||
<th><?= $e($t('receipts.documents.config')) ?></th>
|
||||
<th><?= $e($t('receipts.documents.actions')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($receiptsList as $receipt): ?>
|
||||
<tr>
|
||||
<td><strong><?= $e((string) ($receipt['receipt_number'] ?? '')) ?></strong></td>
|
||||
<td class="text-nowrap"><?= $e((string) ($receipt['issue_date'] ?? '')) ?></td>
|
||||
<td class="text-nowrap"><?= $e($receipt['total_gross'] !== null ? number_format((float) $receipt['total_gross'], 2, '.', ' ') : '-') ?></td>
|
||||
<td><?= $e((string) ($receipt['config_name'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<span class="btn btn--sm btn--secondary btn--disabled"><?= $e($t('receipts.documents.preview')) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($documentsList !== []): ?>
|
||||
<h4 class="section-title mt-12">Dokumenty zewnetrzne</h4>
|
||||
<div class="table-wrap mt-8">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Numer</th>
|
||||
<th>Typ</th>
|
||||
<th>Kwota brutto</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($documentsList as $doc): ?>
|
||||
<tr>
|
||||
<td><strong><?= $e((string) ($doc['document_number'] ?? '-')) ?></strong></td>
|
||||
<td><?= $e((string) ($doc['document_type_id'] ?? '-')) ?></td>
|
||||
<td class="text-nowrap"><?= $e($doc['price_with_tax'] !== null ? number_format((float) $doc['price_with_tax'], 2, '.', ' ') . ' ' . ($doc['currency'] ?? '') : '-') ?></td>
|
||||
<td class="text-nowrap"><?= $e((string) ($doc['source_created_at'] ?? '-')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
158
resources/views/settings/accounting.php
Normal file
158
resources/views/settings/accounting.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
$configs = is_array($configs ?? null) ? $configs : [];
|
||||
$ec = is_array($editConfig ?? null) ? $editConfig : null;
|
||||
$isEdit = $ec !== null;
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title"><?= $e($t('settings.accounting.title')) ?></h2>
|
||||
<p class="muted mt-12"><?= $e($t('settings.accounting.description')) ?></p>
|
||||
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($successMessage)): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('settings.accounting.table.heading')) ?></h3>
|
||||
|
||||
<?php if (count($configs) === 0): ?>
|
||||
<p class="muted mt-12"><?= $e($t('settings.accounting.table.empty')) ?></p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('settings.accounting.table.name')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.number_format')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.numbering_type')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.status')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.actions')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($configs as $cfg): ?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($cfg['name'] ?? '')) ?></td>
|
||||
<td><code><?= $e((string) ($cfg['number_format'] ?? '')) ?></code></td>
|
||||
<td><?= $e($t('settings.accounting.options.numbering_type.' . ($cfg['numbering_type'] ?? 'monthly'))) ?></td>
|
||||
<td>
|
||||
<?php if (((int) ($cfg['is_active'] ?? 0)) === 1): ?>
|
||||
<span class="badge badge--success"><?= $e($t('settings.accounting.options.active')) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted"><?= $e($t('settings.accounting.options.inactive')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/accounting?edit=<?= (int) ($cfg['id'] ?? 0) ?>" class="btn btn--sm btn--secondary"><?= $e($t('settings.accounting.actions.edit')) ?></a>
|
||||
<form action="/settings/accounting/toggle" method="post" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($cfg['id'] ?? 0) ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">
|
||||
<?= ((int) ($cfg['is_active'] ?? 0)) === 1 ? $e($t('settings.accounting.actions.deactivate')) : $e($t('settings.accounting.actions.activate')) ?>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/settings/accounting/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($cfg['id'] ?? 0) ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn"><?= $e($t('settings.accounting.actions.delete')) ?></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $isEdit ? $e($t('settings.accounting.form.edit_heading')) : $e($t('settings.accounting.form.add_heading')) ?></h3>
|
||||
|
||||
<form action="/settings/accounting/save" method="post" novalidate class="mt-12">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int) ($ec['id'] ?? 0) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.name')) ?> *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e((string) ($ec['name'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.number_format')) ?> *</span>
|
||||
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="PAR/%N/%M/%Y" value="<?= $e((string) ($ec['number_format'] ?? 'PAR/%N/%M/%Y')) ?>">
|
||||
<small class="field-hint"><?= $e($t('settings.accounting.fields.number_format_hint')) ?></small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.numbering_type')) ?></span>
|
||||
<select class="form-control" name="numbering_type">
|
||||
<option value="monthly"<?= ((string) ($ec['numbering_type'] ?? 'monthly')) === 'monthly' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.numbering_type.monthly')) ?></option>
|
||||
<option value="yearly"<?= ((string) ($ec['numbering_type'] ?? '')) === 'yearly' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.numbering_type.yearly')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.sale_date_source')) ?></span>
|
||||
<select class="form-control" name="sale_date_source">
|
||||
<option value="issue_date"<?= ((string) ($ec['sale_date_source'] ?? 'issue_date')) === 'issue_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.issue_date')) ?></option>
|
||||
<option value="order_date"<?= ((string) ($ec['sale_date_source'] ?? '')) === 'order_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.order_date')) ?></option>
|
||||
<option value="payment_date"<?= ((string) ($ec['sale_date_source'] ?? '')) === 'payment_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.payment_date')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.order_reference')) ?></span>
|
||||
<select class="form-control" name="order_reference">
|
||||
<option value="none"<?= ((string) ($ec['order_reference'] ?? 'none')) === 'none' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.none')) ?></option>
|
||||
<option value="orderpro"<?= ((string) ($ec['order_reference'] ?? '')) === 'orderpro' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.orderpro')) ?></option>
|
||||
<option value="integration"<?= ((string) ($ec['order_reference'] ?? '')) === 'integration' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.integration')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_named" value="1"<?= ((int) ($ec['is_named'] ?? 0)) === 1 ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0"><?= $e($t('settings.accounting.fields.is_named')) ?></span>
|
||||
</label>
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isEdit ? (((int) ($ec['is_active'] ?? 0)) === 1 ? ' checked' : '') : ' checked' ?>>
|
||||
<span class="field-label" style="margin:0"><?= $e($t('settings.accounting.fields.is_active')) ?></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? $e($t('settings.accounting.actions.save_edit')) : $e($t('settings.accounting.actions.save_new')) ?></button>
|
||||
<?php if ($isEdit): ?>
|
||||
<a href="/settings/accounting" class="btn btn--secondary"><?= $e($t('settings.accounting.actions.cancel')) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var form = this.closest('form');
|
||||
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||
window.OrderProAlerts.confirm(
|
||||
'<?= $e($t('settings.accounting.confirm.delete_title')) ?>',
|
||||
'<?= $e($t('settings.accounting.confirm.delete_message')) ?>',
|
||||
function() { form.submit(); }
|
||||
);
|
||||
} else {
|
||||
if (confirm('<?= $e($t('settings.accounting.confirm.delete_message')) ?>')) {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -72,6 +72,26 @@ $s = is_array($settings ?? null) ? $settings : [];
|
||||
<input class="form-control" type="text" name="tax_number" maxlength="64" value="<?= $e((string) ($s['tax_number'] ?? '')) ?>">
|
||||
</label>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">REGON</span>
|
||||
<input class="form-control" type="text" name="regon" maxlength="14" value="<?= $e((string) ($s['regon'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Numer BDO</span>
|
||||
<input class="form-control" type="text" name="bdo_number" maxlength="20" value="<?= $e((string) ($s['bdo_number'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">KRS / Wpis do ewidencji</span>
|
||||
<input class="form-control" type="text" name="court_register" maxlength="128" value="<?= $e((string) ($s['court_register'] ?? '')) ?>">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label">Logo firmy (sciezka)</span>
|
||||
<input class="form-control" type="text" name="logo_path" maxlength="255" placeholder="np. uploads/logo.png" value="<?= $e((string) ($s['logo_path'] ?? '')) ?>">
|
||||
</label>
|
||||
|
||||
<h3 class="section-title mt-16"><?= $e($t('settings.company.section_bank')) ?></h3>
|
||||
|
||||
<div class="form-grid-2 mt-12">
|
||||
|
||||
@@ -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]);
|
||||
|
||||
263
src/Modules/Accounting/ReceiptController.php
Normal file
263
src/Modules/Accounting/ReceiptController.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Accounting;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use Throwable;
|
||||
|
||||
final class ReceiptController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ReceiptRepository $receipts,
|
||||
private readonly ReceiptConfigRepository $receiptConfigs,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly OrdersRepository $orders
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->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<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|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<string, mixed> $config
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $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<string, mixed> $config
|
||||
* @param array<string, mixed> $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;
|
||||
}
|
||||
}
|
||||
121
src/Modules/Accounting/ReceiptRepository.php
Normal file
121
src/Modules/Accounting/ReceiptRepository.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Accounting;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class ReceiptRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
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<string, mixed>|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<string, mixed> $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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
136
src/Modules/Settings/ReceiptConfigController.php
Normal file
136
src/Modules/Settings/ReceiptConfigController.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
final class ReceiptConfigController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ReceiptConfigRepository $repository
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$t = $this->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');
|
||||
}
|
||||
}
|
||||
99
src/Modules/Settings/ReceiptConfigRepository.php
Normal file
99
src/Modules/Settings/ReceiptConfigRepository.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class ReceiptConfigRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
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<string, mixed>|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<string, mixed> $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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user