Antoni09 commited on
Commit
0f283ae
·
verified ·
1 Parent(s): dc48381

Upload 7 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Roboto-VariableFont_wdth,wght.ttf filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,86 @@
1
- ---
2
- title: Test DB
3
- emoji: 🐨
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: Testing to connect DB
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generator faktur
2
+
3
+ Repozytorium zawiera dwa sposoby wystawiania faktur:
4
+
5
+ 1. prosta aplikacje CLI,
6
+ 2. rozbudowany frontend/ backend, ktory generuje faktury w PDF.
7
+
8
+ ## 1. Aplikacja CLI
9
+
10
+ Niewielkie narzedzie w Pythonie, ktore pomaga zebrac podstawowe dane i wystawic fakture z pojedyncza pozycja (usluga lub towar). Dane firmy zapisywane sa lokalnie w `invoice_data.json`, a kazda wygenerowana faktura trafia do katalogu `invoices/`.
11
+
12
+ ## Wymagania
13
+
14
+ - Python 3.8 lub nowszy
15
+
16
+ ## Pierwsze uruchomienie
17
+
18
+ ```powershell
19
+ python invoice_app.py
20
+ ```
21
+
22
+ 1. Podaj dane firmy (nazwa, adres, NIP, numer konta).
23
+ 2. Ustaw haslo, ktore bedzie wymagane przy kolejnych uruchomieniach.
24
+
25
+ Po zakonczeniu konfiguracji uruchom aplikacje ponownie, aby sie zalogowac i wystawic pierwsza fakture.
26
+
27
+ ## Wystawianie faktury
28
+
29
+ ```powershell
30
+ python invoice_app.py
31
+ ```
32
+
33
+ 1. Zaloguj sie haslem ustawionym wczesniej.
34
+ 2. Podaj opis uslugi/towaru, ilosc oraz cene jednostkowa.
35
+ 3. Opcjonalnie wpisz dane klienta.
36
+
37
+ Aplikacja automatycznie policzy kwote netto i zapisze fakture w katalogu `invoices/` jako plik tekstowy (np. `invoices/FV-20240101-120000.txt`). Dane faktury sa rowniez dopisywane do pliku `invoice_data.json`, dzieki czemu latwo przechowywac historie.
38
+
39
+ ## 2. Aplikacja webowa (frontend + backend)
40
+
41
+ Interfejs przegladarkowy z serwerem REST (Flask), ktory przechowuje dane firmy oraz historie faktur w pliku `web_invoice_store.json`.
42
+
43
+ ### Wymagania
44
+
45
+ - Python 3.8+
46
+ - Zainstalowany pakiet `Flask`
47
+
48
+ ```powershell
49
+ python -m pip install -r requirements.txt
50
+ ```
51
+
52
+ ### Uruchomienie
53
+
54
+ 1. Start serwera API oraz statycznego frontendu:
55
+
56
+ ```powershell
57
+ python server.py
58
+ ```
59
+
60
+ 2. W przegladarce odwiedz `http://localhost:5000/`.
61
+
62
+ ### Funkcje webowego generatora
63
+
64
+ - Pierwsze uruchomienie:
65
+ - konfiguracja danych sprzedawcy (nazwa, adres, kod pocztowy, miejscowosc, NIP, numer konta, haslo),
66
+ - dane przechowywane lokalnie na serwerze w `web_invoice_store.json`.
67
+ - Logowanie chronione haslem (token przechowywany w `sessionStorage` przegladarki).
68
+ - Panel po zalogowaniu:
69
+ - wyswietlenie danych sprzedawcy oraz mozliwosc edycji bezpośrednio z poziomu UI,
70
+ - formularz danych nabywcy z rozszerzonym adresem (ulica, kod pocztowy, miejscowosc, NIP),
71
+ - pole daty sprzedazy/wykonania uslugi (niezalezne od daty wystawienia),
72
+ - pozycje faktury z dynamiczna tabela: wprowadzamy cene brutto, wybieramy stawke VAT (23/8/5/0/ZW/NP), aplikacja automatycznie liczy cene netto, wartosc netto, VAT i brutto,
73
+ - obsluga pozycji zwolnionych (ZW) wraz z polem na podstawe prawna zwolnienia (wyswietlana na fakturze),
74
+ - podsumowanie stawek (np. 23% – X netto / VAT – Y, ZW – Z netto / VAT – 0) zamiast pojedynczej sumy netto,
75
+ - generowanie podgladu faktury oraz eksport do PDF w formacie A4 (NABYWCA po lewej, SPRZEDAWCA po prawej, tabela zgodna z nazewnictwem ustawy: „Cena jedn. netto”, „Wartosc netto (pozycja)”, „Stawka VAT”, „Kwota VAT (pozycja)”, „Wartosc brutto”).
76
+ - Historia faktur zapisywana jest po stronie serwera (ostatnie 200 dokumentow).
77
+
78
+ > **Uwaga:** do wygenerowania pliku PDF wykorzystywana jest biblioteka [jsPDF](https://cdnjs.com/libraries/jspdf) ladowana z CDN. Przegladarka musi miec dostep do internetu, aby pobrac skrypt.
79
+
80
+ ### Reset danych webowych
81
+
82
+ Usun plik `web_invoice_store.json`, aby uruchomic konfiguracje od nowa (spowoduje to utrate historii faktur).
83
+
84
+ ## Reset danych CLI
85
+
86
+ Jezeli chcesz rozpoczac konfiguracje od nowa, usun pliki `invoice_data.json` i katalog `invoices/` (uwaga: spowoduje to utrate historii wystawionych faktur).
Roboto-VariableFont_wdth,wght.ttf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b25986d18730960c7b27384ab2bc500856ae7fe9e71c9850019195ff9019f0b2
3
+ size 468308
RobotoFontBase64.txt ADDED
The diff for this file is too large to render. See raw diff
 
invoice_app.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import getpass
2
+ import hashlib
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+
7
+ DATA_FILE = "invoice_data.json"
8
+ INVOICE_FOLDER = "invoices"
9
+
10
+
11
+ def hash_password(password: str) -> str:
12
+ return hashlib.sha256(password.encode("utf-8")).hexdigest()
13
+
14
+
15
+ def prompt_non_empty(prompt: str) -> str:
16
+ while True:
17
+ value = input(prompt).strip()
18
+ if value:
19
+ return value
20
+ print("Pole nie moze byc puste.")
21
+
22
+
23
+ def prompt_positive_float(prompt: str) -> float:
24
+ while True:
25
+ raw_value = input(prompt).replace(",", ".").strip()
26
+ try:
27
+ value = float(raw_value)
28
+ except ValueError:
29
+ print("Wprowadz liczbe.")
30
+ continue
31
+ if value <= 0:
32
+ print("Wartosc musi byc wieksza od zera.")
33
+ continue
34
+ return value
35
+
36
+
37
+ def load_data():
38
+ if not os.path.exists(DATA_FILE):
39
+ return None
40
+ with open(DATA_FILE, "r", encoding="utf-8") as handle:
41
+ return json.load(handle)
42
+
43
+
44
+ def save_data(data) -> None:
45
+ with open(DATA_FILE, "w", encoding="utf-8") as handle:
46
+ json.dump(data, handle, indent=2, ensure_ascii=False)
47
+
48
+
49
+ def run_setup():
50
+ print("=== Konfiguracja konta przedsiebiorcy ===")
51
+ business = {
52
+ "company_name": prompt_non_empty("Nazwa firmy: "),
53
+ "owner_name": prompt_non_empty("Imie i nazwisko wlasciciela: "),
54
+ "address": prompt_non_empty("Adres: "),
55
+ "tax_id": prompt_non_empty("NIP: "),
56
+ "bank_account": prompt_non_empty("Numer konta bankowego: "),
57
+ }
58
+
59
+ print("\nUstaw haslo do logowania.")
60
+ while True:
61
+ password = getpass.getpass("Haslo: ")
62
+ confirm = getpass.getpass("Powtorz haslo: ")
63
+ if not password:
64
+ print("Haslo nie moze byc puste.")
65
+ continue
66
+ if password != confirm:
67
+ print("Hasla nie sa identyczne. Sprobuj ponownie.")
68
+ continue
69
+ break
70
+
71
+ data = {
72
+ "business": business,
73
+ "password_hash": hash_password(password),
74
+ "invoices": [],
75
+ }
76
+ save_data(data)
77
+ print("\nDane zapisane. Uruchom aplikacje ponownie, aby zalogowac sie i wystawiac faktury.")
78
+
79
+
80
+ def authenticate(data) -> bool:
81
+ for attempt in range(3):
82
+ password = getpass.getpass("Haslo: ")
83
+ if hash_password(password) == data["password_hash"]:
84
+ return True
85
+ print("Nieprawidlowe haslo.")
86
+ return False
87
+
88
+
89
+ def prompt_client_details():
90
+ answer = input("Dodac dane klienta? (T/N): ").strip().lower()
91
+ if answer not in ("t", "tak"):
92
+ return {}
93
+ print("\n=== Dane klienta ===")
94
+ client = {
95
+ "name": prompt_non_empty("Nazwa / Imie i nazwisko: "),
96
+ "address": prompt_non_empty("Adres: "),
97
+ "tax_id": input("NIP (opcjonalnie): ").strip(),
98
+ }
99
+ return client
100
+
101
+
102
+ def format_invoice_text(invoice_id: str, business: dict, invoice: dict) -> str:
103
+ lines = [
104
+ f"Faktura: {invoice_id}",
105
+ f"Data wystawienia: {invoice['issued_at']}",
106
+ "",
107
+ "=== Sprzedawca ===",
108
+ f"Nazwa: {business['company_name']}",
109
+ f"Wlasciciel: {business['owner_name']}",
110
+ f"Adres: {business['address']}",
111
+ f"NIP: {business['tax_id']}",
112
+ f"Konto bankowe: {business['bank_account']}",
113
+ "",
114
+ "=== Nabywca ===",
115
+ ]
116
+
117
+ client = invoice.get("client", {})
118
+ if client:
119
+ lines.extend(
120
+ [
121
+ f"Nazwa: {client.get('name', '')}",
122
+ f"Adres: {client.get('address', '')}",
123
+ f"NIP: {client.get('tax_id', '') or '---'}",
124
+ ]
125
+ )
126
+ else:
127
+ lines.append("Brak danych klienta (pole opcjonalne).")
128
+
129
+ lines.extend(
130
+ [
131
+ "",
132
+ "=== Pozycja ===",
133
+ f"Opis: {invoice['item_description']}",
134
+ f"Ilosc: {invoice['quantity']}",
135
+ f"Cena jednostkowa: {invoice['unit_price']:.2f} PLN",
136
+ f"Wartosc netto: {invoice['net_total']:.2f} PLN",
137
+ ]
138
+ )
139
+ return "\n".join(lines)
140
+
141
+
142
+ def create_invoice(data):
143
+ print("\n=== Wystaw fakture ===")
144
+ description = prompt_non_empty("Opis uslugi / towaru: ")
145
+ quantity = prompt_positive_float("Ilosc: ")
146
+ unit_price = prompt_positive_float("Cena jednostkowa (PLN): ")
147
+ client = prompt_client_details()
148
+
149
+ issued_at = datetime.now().strftime("%Y-%m-%d %H:%M")
150
+ invoice_id = datetime.now().strftime("FV-%Y%m%d-%H%M%S")
151
+ net_total = quantity * unit_price
152
+
153
+ invoice = {
154
+ "invoice_id": invoice_id,
155
+ "issued_at": issued_at,
156
+ "item_description": description,
157
+ "quantity": quantity,
158
+ "unit_price": unit_price,
159
+ "net_total": net_total,
160
+ "client": client,
161
+ }
162
+
163
+ os.makedirs(INVOICE_FOLDER, exist_ok=True)
164
+ invoice_path = os.path.join(INVOICE_FOLDER, f"{invoice_id}.txt")
165
+ with open(invoice_path, "w", encoding="utf-8") as handle:
166
+ handle.write(format_invoice_text(invoice_id, data["business"], invoice))
167
+
168
+ data["invoices"].append(invoice)
169
+ save_data(data)
170
+
171
+ print(f"\nFaktura zapisana do {invoice_path}")
172
+ print(f"Suma do zaplaty: {net_total:.2f} PLN")
173
+
174
+
175
+ def main():
176
+ data = load_data()
177
+ if data is None:
178
+ run_setup()
179
+ return
180
+
181
+ print("=== Logowanie ===")
182
+ if not authenticate(data):
183
+ print("Zbyt wiele nieudanych prob logowania. Zakonczono.")
184
+ return
185
+
186
+ create_invoice(data)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
requirements.txt CHANGED
@@ -1,2 +1 @@
1
- fastapi
2
- uvicorn[standard]
 
1
+ Flask>=2.3,<3.0
 
server.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+ import os
4
+ import uuid
5
+ from datetime import datetime
6
+ from decimal import Decimal, ROUND_HALF_UP, getcontext
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ from flask import Flask, jsonify, request, send_from_directory
11
+
12
+ APP_ROOT = Path(__file__).parent.resolve()
13
+ DATA_FILE = APP_ROOT / "web_invoice_store.json"
14
+ INVOICE_HISTORY_LIMIT = 200
15
+
16
+ VAT_RATES: Dict[str, Optional[Decimal]] = {
17
+ "23": Decimal("0.23"),
18
+ "8": Decimal("0.08"),
19
+ "5": Decimal("0.05"),
20
+ "0": Decimal("0.00"),
21
+ "ZW": None,
22
+ "NP": None,
23
+ }
24
+
25
+ SESSION_TOKENS: Dict[str, datetime] = {}
26
+
27
+ ALLOWED_STATIC = {
28
+ "index.html",
29
+ "styles.css",
30
+ "main.js",
31
+ "favicon.ico",
32
+ "Roboto-VariableFont_wdth,wght.ttf",
33
+ }
34
+
35
+
36
+ app = Flask(__name__, static_folder=str(APP_ROOT), static_url_path="")
37
+
38
+ getcontext().prec = 10
39
+
40
+
41
+ def _quantize(value: Decimal) -> Decimal:
42
+ return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
43
+
44
+
45
+ def _decimal(value: Any) -> Decimal:
46
+ try:
47
+ return Decimal(str(value))
48
+ except Exception as error: # pragma: no cover - defensive
49
+ raise ValueError(f"Niepoprawna wartosc liczby: {value}") from error
50
+
51
+
52
+ def hash_password(password: str) -> str:
53
+ return hashlib.sha256(password.encode("utf-8")).hexdigest()
54
+
55
+
56
+ def load_store() -> Dict[str, Any]:
57
+ if not DATA_FILE.exists():
58
+ return {"business": None, "password_hash": None, "invoices": []}
59
+ with DATA_FILE.open("r", encoding="utf-8") as handle:
60
+ return json.load(handle)
61
+
62
+
63
+ def save_store(data: Dict[str, Any]) -> None:
64
+ DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
65
+ with DATA_FILE.open("w", encoding="utf-8") as handle:
66
+ json.dump(data, handle, ensure_ascii=False, indent=2)
67
+
68
+
69
+ def ensure_configured(data: Dict[str, Any]) -> None:
70
+ if not data.get("business") or not data.get("password_hash"):
71
+ raise ValueError("Aplikacja nie zostala skonfigurowana.")
72
+
73
+
74
+ def parse_iso_date(value: Optional[str]) -> Optional[str]:
75
+ if not value:
76
+ return None
77
+ try:
78
+ parsed = datetime.fromisoformat(value)
79
+ return parsed.strftime("%Y-%m-%d")
80
+ except ValueError:
81
+ return None
82
+
83
+
84
+ def compute_invoice_items(items_payload: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Decimal]]]:
85
+ if not items_payload:
86
+ raise ValueError("Dodaj przynajmniej jedna pozycje.")
87
+
88
+ computed_items: List[Dict[str, Any]] = []
89
+ summary: Dict[str, Dict[str, Decimal]] = {}
90
+
91
+ for raw in items_payload:
92
+ name = (raw.get("name") or "").strip()
93
+ if not name:
94
+ raise ValueError("Kazda pozycja musi miec nazwe.")
95
+
96
+ quantity = _decimal(raw.get("quantity", "0"))
97
+ if quantity <= 0:
98
+ raise ValueError("Ilosc musi byc wieksza od zera.")
99
+
100
+ vat_code = str(raw.get("vat_code", "")).upper()
101
+ if vat_code not in VAT_RATES:
102
+ raise ValueError(f"Nieznana stawka VAT: {vat_code}")
103
+
104
+ unit_price_gross = _decimal(raw.get("unit_price_gross", "0"))
105
+ if unit_price_gross <= 0:
106
+ raise ValueError("Cena brutto musi byc wieksza od zera.")
107
+
108
+ rate = VAT_RATES[vat_code]
109
+ if rate is None:
110
+ unit_price_net = unit_price_gross
111
+ vat_amount = Decimal("0.00")
112
+ else:
113
+ unit_price_net = unit_price_gross / (Decimal("1.00") + rate)
114
+ vat_amount = unit_price_gross - unit_price_net
115
+
116
+ unit_price_net = _quantize(unit_price_net)
117
+ unit_price_gross = _quantize(unit_price_gross)
118
+
119
+ net_total = _quantize(unit_price_net * quantity)
120
+ vat_amount_total = _quantize(vat_amount * quantity if rate is not None else Decimal("0.00"))
121
+ gross_total = _quantize(unit_price_gross * quantity)
122
+
123
+ vat_label = "ZW" if vat_code in {"ZW", "0"} else ("NP" if vat_code == "NP" else f"{vat_code}%")
124
+
125
+ computed_items.append(
126
+ {
127
+ "name": name,
128
+ "quantity": str(_quantize(quantity)),
129
+ "vat_code": vat_code,
130
+ "vat_label": vat_label,
131
+ "unit_price_net": str(unit_price_net),
132
+ "unit_price_gross": str(unit_price_gross),
133
+ "net_total": str(net_total),
134
+ "vat_amount": str(vat_amount_total),
135
+ "gross_total": str(gross_total),
136
+ }
137
+ )
138
+
139
+ summary_key = vat_label
140
+ bucket = summary.setdefault(summary_key, {"net": Decimal("0.00"), "vat": Decimal("0.00"), "gross": Decimal("0.00")})
141
+ bucket["net"] += net_total
142
+ bucket["vat"] += vat_amount_total
143
+ bucket["gross"] += gross_total
144
+
145
+ return computed_items, summary
146
+
147
+
148
+ def computed_summary_to_serializable(summary: Dict[str, Dict[str, Decimal]]) -> List[Dict[str, str]]:
149
+ serialized = []
150
+ for vat_label, values in summary.items():
151
+ serialized.append(
152
+ {
153
+ "vat_label": vat_label,
154
+ "net_total": str(_quantize(values["net"])),
155
+ "vat_total": str(_quantize(values["vat"])),
156
+ "gross_total": str(_quantize(values["gross"])),
157
+ }
158
+ )
159
+ serialized.sort(key=lambda item: item["vat_label"])
160
+ return serialized
161
+
162
+
163
+ def compute_invoice(payload: Dict[str, Any], business: Dict[str, Any]) -> Dict[str, Any]:
164
+ items_payload = payload.get("items", [])
165
+ computed_items, summary = compute_invoice_items(items_payload)
166
+
167
+ net_sum = sum(Decimal(item["net_total"]) for item in computed_items)
168
+ vat_sum = sum(Decimal(item["vat_amount"]) for item in computed_items)
169
+ gross_sum = sum(Decimal(item["gross_total"]) for item in computed_items)
170
+
171
+ issued_at = datetime.now()
172
+ invoice_id = issued_at.strftime("FV-%Y%m%d-%H%M%S")
173
+
174
+ sale_date = parse_iso_date(payload.get("sale_date")) or issued_at.strftime("%Y-%m-%d")
175
+ client_payload = payload.get("client") or {}
176
+ client = {
177
+ "name": (client_payload.get("name") or "").strip(),
178
+ "address_line": (client_payload.get("address_line") or "").strip(),
179
+ "postal_code": (client_payload.get("postal_code") or "").strip(),
180
+ "city": (client_payload.get("city") or "").strip(),
181
+ "tax_id": (client_payload.get("tax_id") or "").strip(),
182
+ }
183
+
184
+ invoice = {
185
+ "invoice_id": invoice_id,
186
+ "issued_at": issued_at.strftime("%Y-%m-%d %H:%M"),
187
+ "sale_date": sale_date,
188
+ "items": computed_items,
189
+ "summary": computed_summary_to_serializable(summary),
190
+ "totals": {
191
+ "net": str(_quantize(net_sum)),
192
+ "vat": str(_quantize(vat_sum)),
193
+ "gross": str(_quantize(gross_sum)),
194
+ },
195
+ "client": client,
196
+ "exemption_note": (payload.get("exemption_note") or "").strip(),
197
+ }
198
+
199
+ return invoice
200
+
201
+
202
+ def create_token() -> str:
203
+ token = uuid.uuid4().hex
204
+ SESSION_TOKENS[token] = datetime.now()
205
+ return token
206
+
207
+
208
+ def get_token() -> Optional[str]:
209
+ header = request.headers.get("Authorization", "")
210
+ if not header.startswith("Bearer "):
211
+ return None
212
+ return header.split(" ", 1)[1].strip()
213
+
214
+
215
+ def require_auth() -> str:
216
+ token = get_token()
217
+ if not token or token not in SESSION_TOKENS:
218
+ raise PermissionError("Brak autoryzacji.")
219
+ return token
220
+
221
+
222
+ @app.route("/")
223
+ def serve_index() -> Any:
224
+ return send_from_directory(app.static_folder, "index.html")
225
+
226
+
227
+ @app.route("/<path:path>")
228
+ def serve_static(path: str) -> Any:
229
+ if path not in ALLOWED_STATIC:
230
+ return jsonify({"error": "Nie znaleziono pliku."}), 404
231
+ target = Path(app.static_folder) / path
232
+ if target.is_file():
233
+ return send_from_directory(app.static_folder, path)
234
+ return jsonify({"error": "Nie znaleziono pliku."}), 404
235
+
236
+
237
+ @app.route("/api/status", methods=["GET"])
238
+ def api_status() -> Any:
239
+ data = load_store()
240
+ configured = bool(data.get("business") and data.get("password_hash"))
241
+ return jsonify({"configured": configured})
242
+
243
+
244
+ @app.route("/api/setup", methods=["POST"])
245
+ def api_setup() -> Any:
246
+ data = load_store()
247
+ if data.get("password_hash"):
248
+ return jsonify({"error": "Aplikacja zostala juz skonfigurowana."}), 400
249
+
250
+ payload = request.get_json(force=True)
251
+ required_fields = [
252
+ "company_name",
253
+ "owner_name",
254
+ "address_line",
255
+ "postal_code",
256
+ "city",
257
+ "tax_id",
258
+ "bank_account",
259
+ "password",
260
+ ]
261
+
262
+ missing = [field for field in required_fields if not (payload.get(field) or "").strip()]
263
+ if missing:
264
+ return jsonify({"error": f"Brakuje pol: {', '.join(missing)}"}), 400
265
+
266
+ if len(payload["password"]) < 4:
267
+ return jsonify({"error": "Haslo musi miec co najmniej 4 znaki."}), 400
268
+
269
+ data["business"] = {
270
+ "company_name": payload["company_name"].strip(),
271
+ "owner_name": payload["owner_name"].strip(),
272
+ "address_line": payload["address_line"].strip(),
273
+ "postal_code": payload["postal_code"].strip(),
274
+ "city": payload["city"].strip(),
275
+ "tax_id": payload["tax_id"].strip(),
276
+ "bank_account": payload["bank_account"].strip(),
277
+ }
278
+ data["password_hash"] = hash_password(payload["password"])
279
+ data.setdefault("invoices", [])
280
+
281
+ save_store(data)
282
+ return jsonify({"message": "Dane zapisane. Mozesz sie zalogowac."})
283
+
284
+
285
+ @app.route("/api/login", methods=["POST"])
286
+ def api_login() -> Any:
287
+ payload = request.get_json(force=True)
288
+ password = (payload.get("password") or "").strip()
289
+ data = load_store()
290
+
291
+ if not data.get("password_hash"):
292
+ return jsonify({"error": "Aplikacja nie zostala skonfigurowana."}), 400
293
+
294
+ if hash_password(password) != data["password_hash"]:
295
+ return jsonify({"error": "Nieprawidlowe haslo."}), 401
296
+
297
+ token = create_token()
298
+ return jsonify({"token": token})
299
+
300
+
301
+ @app.route("/api/business", methods=["GET", "PUT"])
302
+ def api_business() -> Any:
303
+ try:
304
+ require_auth()
305
+ except PermissionError:
306
+ return jsonify({"error": "Brak autoryzacji."}), 401
307
+
308
+ data = load_store()
309
+ if request.method == "GET":
310
+ ensure_configured(data)
311
+ return jsonify({"business": data["business"]})
312
+
313
+ payload = request.get_json(force=True)
314
+ current = data.get("business") or {}
315
+ updated = {
316
+ "company_name": (payload.get("company_name") or current.get("company_name") or "").strip(),
317
+ "owner_name": (payload.get("owner_name") or current.get("owner_name") or "").strip(),
318
+ "address_line": (payload.get("address_line") or current.get("address_line") or "").strip(),
319
+ "postal_code": (payload.get("postal_code") or current.get("postal_code") or "").strip(),
320
+ "city": (payload.get("city") or current.get("city") or "").strip(),
321
+ "tax_id": (payload.get("tax_id") or current.get("tax_id") or "").strip(),
322
+ "bank_account": (payload.get("bank_account") or current.get("bank_account") or "").strip(),
323
+ }
324
+
325
+ missing = [field for field, value in updated.items() if not value]
326
+ if missing:
327
+ return jsonify({"error": f"Wypelnij wszystkie pola: {', '.join(missing)}"}), 400
328
+
329
+ data["business"] = updated
330
+ save_store(data)
331
+ return jsonify({"business": updated})
332
+
333
+
334
+ @app.route("/api/invoices", methods=["POST", "GET"])
335
+ def api_invoices() -> Any:
336
+ try:
337
+ require_auth()
338
+ except PermissionError:
339
+ return jsonify({"error": "Brak autoryzacji."}), 401
340
+
341
+ data = load_store()
342
+ ensure_configured(data)
343
+
344
+ if request.method == "GET":
345
+ return jsonify({"invoices": data.get("invoices", [])})
346
+
347
+ payload = request.get_json(force=True)
348
+ try:
349
+ invoice = compute_invoice(payload, data["business"])
350
+ except ValueError as error:
351
+ return jsonify({"error": str(error)}), 400
352
+
353
+ invoices = data.setdefault("invoices", [])
354
+ invoices.append(invoice)
355
+ if len(invoices) > INVOICE_HISTORY_LIMIT:
356
+ data["invoices"] = invoices[-INVOICE_HISTORY_LIMIT:]
357
+
358
+ save_store(data)
359
+ return jsonify({"invoice": invoice})
360
+
361
+
362
+ if __name__ == "__main__":
363
+ port = int(os.environ.get("PORT", "5000"))
364
+ app.run(host="0.0.0.0", port=port, debug=True)
web_invoice_store.json ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "business": {
3
+ "company_name": "Przykładowa firma",
4
+ "owner_name": "Anton Test",
5
+ "address_line": "Ul. Spółdzielcza 4/7",
6
+ "postal_code": "44-100",
7
+ "city": "Gliwice",
8
+ "tax_id": "777-777-777",
9
+ "bank_account": "481000100010001000"
10
+ },
11
+ "password_hash": "b074fdc0ca844d18429850722969174fe104cbc4b6243b4f820e8fc1c095a859",
12
+ "invoices": [
13
+ {
14
+ "invoice_id": "FV-20251026-184821",
15
+ "issued_at": "2025-10-26 18:48",
16
+ "sale_date": "2025-10-26",
17
+ "items": [
18
+ {
19
+ "name": "Usługa podologiczna",
20
+ "quantity": "1.00",
21
+ "vat_code": "ZW",
22
+ "vat_label": "ZW",
23
+ "unit_price_net": "140.00",
24
+ "unit_price_gross": "140.00",
25
+ "net_total": "140.00",
26
+ "vat_amount": "0.00",
27
+ "gross_total": "140.00"
28
+ },
29
+ {
30
+ "name": "Krem do nóg",
31
+ "quantity": "1.00",
32
+ "vat_code": "0",
33
+ "vat_label": "ZW",
34
+ "unit_price_net": "33.00",
35
+ "unit_price_gross": "33.00",
36
+ "net_total": "33.00",
37
+ "vat_amount": "0.00",
38
+ "gross_total": "33.00"
39
+ }
40
+ ],
41
+ "summary": [
42
+ {
43
+ "vat_label": "ZW",
44
+ "net_total": "173.00",
45
+ "vat_total": "0.00",
46
+ "gross_total": "173.00"
47
+ }
48
+ ],
49
+ "totals": {
50
+ "net": "173.00",
51
+ "vat": "0.00",
52
+ "gross": "173.00"
53
+ },
54
+ "client": {
55
+ "name": "Piotr Petrowicz",
56
+ "address_line": "Grottgera",
57
+ "postal_code": "44-100",
58
+ "city": "Gliwice",
59
+ "tax_id": "777-777-777"
60
+ },
61
+ "exemption_note": "Nieprzekroczenie 200.000 PLN obroku/rok"
62
+ },
63
+ {
64
+ "invoice_id": "FV-20251026-185431",
65
+ "issued_at": "2025-10-26 18:54",
66
+ "sale_date": "2025-10-26",
67
+ "items": [
68
+ {
69
+ "name": "usługa podologiczna",
70
+ "quantity": "1.00",
71
+ "vat_code": "23",
72
+ "vat_label": "23%",
73
+ "unit_price_net": "105.69",
74
+ "unit_price_gross": "130.00",
75
+ "net_total": "105.69",
76
+ "vat_amount": "24.31",
77
+ "gross_total": "130.00"
78
+ }
79
+ ],
80
+ "summary": [
81
+ {
82
+ "vat_label": "23%",
83
+ "net_total": "105.69",
84
+ "vat_total": "24.31",
85
+ "gross_total": "130.00"
86
+ }
87
+ ],
88
+ "totals": {
89
+ "net": "105.69",
90
+ "vat": "24.31",
91
+ "gross": "130.00"
92
+ },
93
+ "client": {
94
+ "name": "Piotr Petrowicz",
95
+ "address_line": "Grottgera",
96
+ "postal_code": "44-100",
97
+ "city": "Gliwice",
98
+ "tax_id": "444-444-444"
99
+ },
100
+ "exemption_note": ""
101
+ },
102
+ {
103
+ "invoice_id": "FV-20251026-185949",
104
+ "issued_at": "2025-10-26 18:59",
105
+ "sale_date": "2025-10-26",
106
+ "items": [
107
+ {
108
+ "name": "Usługa podologiczna",
109
+ "quantity": "2.00",
110
+ "vat_code": "0",
111
+ "vat_label": "ZW",
112
+ "unit_price_net": "130.00",
113
+ "unit_price_gross": "130.00",
114
+ "net_total": "260.00",
115
+ "vat_amount": "0.00",
116
+ "gross_total": "260.00"
117
+ }
118
+ ],
119
+ "summary": [
120
+ {
121
+ "vat_label": "ZW",
122
+ "net_total": "260.00",
123
+ "vat_total": "0.00",
124
+ "gross_total": "260.00"
125
+ }
126
+ ],
127
+ "totals": {
128
+ "net": "260.00",
129
+ "vat": "0.00",
130
+ "gross": "260.00"
131
+ },
132
+ "client": {
133
+ "name": "Piotr Petrowicz",
134
+ "address_line": "Grottgera",
135
+ "postal_code": "44-100",
136
+ "city": "Gliwice",
137
+ "tax_id": "555-555-555"
138
+ },
139
+ "exemption_note": "Brak 200.000 obrotu/rok"
140
+ },
141
+ {
142
+ "invoice_id": "FV-20251026-190655",
143
+ "issued_at": "2025-10-26 19:06",
144
+ "sale_date": "2025-10-26",
145
+ "items": [
146
+ {
147
+ "name": "usługa podologiczna",
148
+ "quantity": "1.00",
149
+ "vat_code": "0",
150
+ "vat_label": "ZW",
151
+ "unit_price_net": "140.00",
152
+ "unit_price_gross": "140.00",
153
+ "net_total": "140.00",
154
+ "vat_amount": "0.00",
155
+ "gross_total": "140.00"
156
+ }
157
+ ],
158
+ "summary": [
159
+ {
160
+ "vat_label": "ZW",
161
+ "net_total": "140.00",
162
+ "vat_total": "0.00",
163
+ "gross_total": "140.00"
164
+ }
165
+ ],
166
+ "totals": {
167
+ "net": "140.00",
168
+ "vat": "0.00",
169
+ "gross": "140.00"
170
+ },
171
+ "client": {
172
+ "name": "Piotr Petrowicz",
173
+ "address_line": "Grottgera",
174
+ "postal_code": "44-100",
175
+ "city": "Gliwice",
176
+ "tax_id": "444-444-444"
177
+ },
178
+ "exemption_note": "brak 200.000 obrotu/rok"
179
+ },
180
+ {
181
+ "invoice_id": "FV-20251026-190928",
182
+ "issued_at": "2025-10-26 19:09",
183
+ "sale_date": "2025-10-26",
184
+ "items": [
185
+ {
186
+ "name": "usługa podologiczna",
187
+ "quantity": "1.00",
188
+ "vat_code": "ZW",
189
+ "vat_label": "ZW",
190
+ "unit_price_net": "130.00",
191
+ "unit_price_gross": "130.00",
192
+ "net_total": "130.00",
193
+ "vat_amount": "0.00",
194
+ "gross_total": "130.00"
195
+ }
196
+ ],
197
+ "summary": [
198
+ {
199
+ "vat_label": "ZW",
200
+ "net_total": "130.00",
201
+ "vat_total": "0.00",
202
+ "gross_total": "130.00"
203
+ }
204
+ ],
205
+ "totals": {
206
+ "net": "130.00",
207
+ "vat": "0.00",
208
+ "gross": "130.00"
209
+ },
210
+ "client": {
211
+ "name": "Piotr Petrowicz",
212
+ "address_line": "Grottgera",
213
+ "postal_code": "44-100",
214
+ "city": "Gliwice",
215
+ "tax_id": "111-111-111"
216
+ },
217
+ "exemption_note": "brak obrotu 200.000PLN/rok"
218
+ },
219
+ {
220
+ "invoice_id": "FV-20251026-191200",
221
+ "issued_at": "2025-10-26 19:12",
222
+ "sale_date": "2025-10-26",
223
+ "items": [
224
+ {
225
+ "name": "Usługa podologiczna",
226
+ "quantity": "1.00",
227
+ "vat_code": "0",
228
+ "vat_label": "ZW",
229
+ "unit_price_net": "130.00",
230
+ "unit_price_gross": "130.00",
231
+ "net_total": "130.00",
232
+ "vat_amount": "0.00",
233
+ "gross_total": "130.00"
234
+ }
235
+ ],
236
+ "summary": [
237
+ {
238
+ "vat_label": "ZW",
239
+ "net_total": "130.00",
240
+ "vat_total": "0.00",
241
+ "gross_total": "130.00"
242
+ }
243
+ ],
244
+ "totals": {
245
+ "net": "130.00",
246
+ "vat": "0.00",
247
+ "gross": "130.00"
248
+ },
249
+ "client": {
250
+ "name": "Piotr Petrowicz",
251
+ "address_line": "Grottgera",
252
+ "postal_code": "44-100",
253
+ "city": "Gliwice",
254
+ "tax_id": ""
255
+ },
256
+ "exemption_note": "Brak obrotu 200.000PLN/rok"
257
+ },
258
+ {
259
+ "invoice_id": "FV-20251026-191906",
260
+ "issued_at": "2025-10-26 19:19",
261
+ "sale_date": "2025-10-26",
262
+ "items": [
263
+ {
264
+ "name": "Usługa podologiczna",
265
+ "quantity": "1.00",
266
+ "vat_code": "0",
267
+ "vat_label": "ZW",
268
+ "unit_price_net": "260.00",
269
+ "unit_price_gross": "260.00",
270
+ "net_total": "260.00",
271
+ "vat_amount": "0.00",
272
+ "gross_total": "260.00"
273
+ }
274
+ ],
275
+ "summary": [
276
+ {
277
+ "vat_label": "ZW",
278
+ "net_total": "260.00",
279
+ "vat_total": "0.00",
280
+ "gross_total": "260.00"
281
+ }
282
+ ],
283
+ "totals": {
284
+ "net": "260.00",
285
+ "vat": "0.00",
286
+ "gross": "260.00"
287
+ },
288
+ "client": {
289
+ "name": "Piotr Petrowicz",
290
+ "address_line": "Grottgera",
291
+ "postal_code": "44-100",
292
+ "city": "Gliwice",
293
+ "tax_id": ""
294
+ },
295
+ "exemption_note": "Brak 200.000PLN/rok obrotu"
296
+ },
297
+ {
298
+ "invoice_id": "FV-20251026-192152",
299
+ "issued_at": "2025-10-26 19:21",
300
+ "sale_date": "2025-10-26",
301
+ "items": [
302
+ {
303
+ "name": "Usługa podologiczna",
304
+ "quantity": "1.00",
305
+ "vat_code": "ZW",
306
+ "vat_label": "ZW",
307
+ "unit_price_net": "140.00",
308
+ "unit_price_gross": "140.00",
309
+ "net_total": "140.00",
310
+ "vat_amount": "0.00",
311
+ "gross_total": "140.00"
312
+ }
313
+ ],
314
+ "summary": [
315
+ {
316
+ "vat_label": "ZW",
317
+ "net_total": "140.00",
318
+ "vat_total": "0.00",
319
+ "gross_total": "140.00"
320
+ }
321
+ ],
322
+ "totals": {
323
+ "net": "140.00",
324
+ "vat": "0.00",
325
+ "gross": "140.00"
326
+ },
327
+ "client": {
328
+ "name": "Piotr S",
329
+ "address_line": "ul. Puszkina",
330
+ "postal_code": "44-100",
331
+ "city": "Gliwice",
332
+ "tax_id": ""
333
+ },
334
+ "exemption_note": "Brak obrotu 200.000 PLN/rok"
335
+ },
336
+ {
337
+ "invoice_id": "FV-20251026-192644",
338
+ "issued_at": "2025-10-26 19:26",
339
+ "sale_date": "2025-10-26",
340
+ "items": [
341
+ {
342
+ "name": "Usługa podologiczna",
343
+ "quantity": "1.00",
344
+ "vat_code": "0",
345
+ "vat_label": "ZW",
346
+ "unit_price_net": "140.00",
347
+ "unit_price_gross": "140.00",
348
+ "net_total": "140.00",
349
+ "vat_amount": "0.00",
350
+ "gross_total": "140.00"
351
+ }
352
+ ],
353
+ "summary": [
354
+ {
355
+ "vat_label": "ZW",
356
+ "net_total": "140.00",
357
+ "vat_total": "0.00",
358
+ "gross_total": "140.00"
359
+ }
360
+ ],
361
+ "totals": {
362
+ "net": "140.00",
363
+ "vat": "0.00",
364
+ "gross": "140.00"
365
+ },
366
+ "client": {
367
+ "name": "Piotr Petrowicz",
368
+ "address_line": "Grottgera",
369
+ "postal_code": "44-100",
370
+ "city": "Gliwice",
371
+ "tax_id": ""
372
+ },
373
+ "exemption_note": "Brak obrotu 200.000 PLN"
374
+ },
375
+ {
376
+ "invoice_id": "FV-20251026-193131",
377
+ "issued_at": "2025-10-26 19:31",
378
+ "sale_date": "2025-10-26",
379
+ "items": [
380
+ {
381
+ "name": "Usługa podologiczna",
382
+ "quantity": "1.00",
383
+ "vat_code": "23",
384
+ "vat_label": "23%",
385
+ "unit_price_net": "113.82",
386
+ "unit_price_gross": "140.00",
387
+ "net_total": "113.82",
388
+ "vat_amount": "26.18",
389
+ "gross_total": "140.00"
390
+ }
391
+ ],
392
+ "summary": [
393
+ {
394
+ "vat_label": "23%",
395
+ "net_total": "113.82",
396
+ "vat_total": "26.18",
397
+ "gross_total": "140.00"
398
+ }
399
+ ],
400
+ "totals": {
401
+ "net": "113.82",
402
+ "vat": "26.18",
403
+ "gross": "140.00"
404
+ },
405
+ "client": {
406
+ "name": "Piotr Petrowicz",
407
+ "address_line": "Grottgera",
408
+ "postal_code": "44-100",
409
+ "city": "Gliwice",
410
+ "tax_id": ""
411
+ },
412
+ "exemption_note": ""
413
+ },
414
+ {
415
+ "invoice_id": "FV-20251026-193336",
416
+ "issued_at": "2025-10-26 19:33",
417
+ "sale_date": "2025-10-26",
418
+ "items": [
419
+ {
420
+ "name": "Usługa podologiczna",
421
+ "quantity": "1.01",
422
+ "vat_code": "23",
423
+ "vat_label": "23%",
424
+ "unit_price_net": "105.69",
425
+ "unit_price_gross": "130.00",
426
+ "net_total": "106.75",
427
+ "vat_amount": "24.55",
428
+ "gross_total": "131.30"
429
+ }
430
+ ],
431
+ "summary": [
432
+ {
433
+ "vat_label": "23%",
434
+ "net_total": "106.75",
435
+ "vat_total": "24.55",
436
+ "gross_total": "131.30"
437
+ }
438
+ ],
439
+ "totals": {
440
+ "net": "106.75",
441
+ "vat": "24.55",
442
+ "gross": "131.30"
443
+ },
444
+ "client": {
445
+ "name": "Piotr Petrowicz",
446
+ "address_line": "Grottgera",
447
+ "postal_code": "44-100",
448
+ "city": "Gliwice",
449
+ "tax_id": ""
450
+ },
451
+ "exemption_note": ""
452
+ },
453
+ {
454
+ "invoice_id": "FV-20251026-193606",
455
+ "issued_at": "2025-10-26 19:36",
456
+ "sale_date": "2025-10-26",
457
+ "items": [
458
+ {
459
+ "name": "Usługa podologiczna",
460
+ "quantity": "1.00",
461
+ "vat_code": "23",
462
+ "vat_label": "23%",
463
+ "unit_price_net": "121.95",
464
+ "unit_price_gross": "150.00",
465
+ "net_total": "121.95",
466
+ "vat_amount": "28.05",
467
+ "gross_total": "150.00"
468
+ }
469
+ ],
470
+ "summary": [
471
+ {
472
+ "vat_label": "23%",
473
+ "net_total": "121.95",
474
+ "vat_total": "28.05",
475
+ "gross_total": "150.00"
476
+ }
477
+ ],
478
+ "totals": {
479
+ "net": "121.95",
480
+ "vat": "28.05",
481
+ "gross": "150.00"
482
+ },
483
+ "client": {
484
+ "name": "Piotr Brzoska",
485
+ "address_line": "Grottgera 33",
486
+ "postal_code": "44-105",
487
+ "city": "Gliwice",
488
+ "tax_id": ""
489
+ },
490
+ "exemption_note": ""
491
+ },
492
+ {
493
+ "invoice_id": "FV-20251026-193913",
494
+ "issued_at": "2025-10-26 19:39",
495
+ "sale_date": "2025-10-26",
496
+ "items": [
497
+ {
498
+ "name": "Usługa podologiczna",
499
+ "quantity": "1.00",
500
+ "vat_code": "23",
501
+ "vat_label": "23%",
502
+ "unit_price_net": "121.95",
503
+ "unit_price_gross": "150.00",
504
+ "net_total": "121.95",
505
+ "vat_amount": "28.05",
506
+ "gross_total": "150.00"
507
+ }
508
+ ],
509
+ "summary": [
510
+ {
511
+ "vat_label": "23%",
512
+ "net_total": "121.95",
513
+ "vat_total": "28.05",
514
+ "gross_total": "150.00"
515
+ }
516
+ ],
517
+ "totals": {
518
+ "net": "121.95",
519
+ "vat": "28.05",
520
+ "gross": "150.00"
521
+ },
522
+ "client": {
523
+ "name": "Piotr Brzoska",
524
+ "address_line": "Grottgera 33",
525
+ "postal_code": "44-105",
526
+ "city": "Gliwice",
527
+ "tax_id": ""
528
+ },
529
+ "exemption_note": ""
530
+ },
531
+ {
532
+ "invoice_id": "FV-20251026-194126",
533
+ "issued_at": "2025-10-26 19:41",
534
+ "sale_date": "2025-10-26",
535
+ "items": [
536
+ {
537
+ "name": "Usługa podologiczna",
538
+ "quantity": "1.00",
539
+ "vat_code": "23",
540
+ "vat_label": "23%",
541
+ "unit_price_net": "121.95",
542
+ "unit_price_gross": "150.00",
543
+ "net_total": "121.95",
544
+ "vat_amount": "28.05",
545
+ "gross_total": "150.00"
546
+ }
547
+ ],
548
+ "summary": [
549
+ {
550
+ "vat_label": "23%",
551
+ "net_total": "121.95",
552
+ "vat_total": "28.05",
553
+ "gross_total": "150.00"
554
+ }
555
+ ],
556
+ "totals": {
557
+ "net": "121.95",
558
+ "vat": "28.05",
559
+ "gross": "150.00"
560
+ },
561
+ "client": {
562
+ "name": "Piotr Brzoska",
563
+ "address_line": "Grottgera 33",
564
+ "postal_code": "44-105",
565
+ "city": "Gliwice",
566
+ "tax_id": ""
567
+ },
568
+ "exemption_note": ""
569
+ },
570
+ {
571
+ "invoice_id": "FV-20251026-194201",
572
+ "issued_at": "2025-10-26 19:42",
573
+ "sale_date": "2025-10-26",
574
+ "items": [
575
+ {
576
+ "name": "Usługa podologiczna",
577
+ "quantity": "1.00",
578
+ "vat_code": "23",
579
+ "vat_label": "23%",
580
+ "unit_price_net": "121.95",
581
+ "unit_price_gross": "150.00",
582
+ "net_total": "121.95",
583
+ "vat_amount": "28.05",
584
+ "gross_total": "150.00"
585
+ }
586
+ ],
587
+ "summary": [
588
+ {
589
+ "vat_label": "23%",
590
+ "net_total": "121.95",
591
+ "vat_total": "28.05",
592
+ "gross_total": "150.00"
593
+ }
594
+ ],
595
+ "totals": {
596
+ "net": "121.95",
597
+ "vat": "28.05",
598
+ "gross": "150.00"
599
+ },
600
+ "client": {
601
+ "name": "Piotr Brzoska",
602
+ "address_line": "Grottgera 33",
603
+ "postal_code": "44-105",
604
+ "city": "Gliwice",
605
+ "tax_id": ""
606
+ },
607
+ "exemption_note": ""
608
+ },
609
+ {
610
+ "invoice_id": "FV-20251026-200308",
611
+ "issued_at": "2025-10-26 20:03",
612
+ "sale_date": "2025-10-26",
613
+ "items": [
614
+ {
615
+ "name": "Usługa podologiczna",
616
+ "quantity": "1.00",
617
+ "vat_code": "23",
618
+ "vat_label": "23%",
619
+ "unit_price_net": "121.95",
620
+ "unit_price_gross": "150.00",
621
+ "net_total": "121.95",
622
+ "vat_amount": "28.05",
623
+ "gross_total": "150.00"
624
+ }
625
+ ],
626
+ "summary": [
627
+ {
628
+ "vat_label": "23%",
629
+ "net_total": "121.95",
630
+ "vat_total": "28.05",
631
+ "gross_total": "150.00"
632
+ }
633
+ ],
634
+ "totals": {
635
+ "net": "121.95",
636
+ "vat": "28.05",
637
+ "gross": "150.00"
638
+ },
639
+ "client": {
640
+ "name": "Piotr Brzoska",
641
+ "address_line": "Grottgera 33",
642
+ "postal_code": "44-105",
643
+ "city": "Gliwice",
644
+ "tax_id": ""
645
+ },
646
+ "exemption_note": ""
647
+ },
648
+ {
649
+ "invoice_id": "FV-20251026-201219",
650
+ "issued_at": "2025-10-26 20:12",
651
+ "sale_date": "2025-10-26",
652
+ "items": [
653
+ {
654
+ "name": "Usługa podologiczna",
655
+ "quantity": "1.00",
656
+ "vat_code": "23",
657
+ "vat_label": "23%",
658
+ "unit_price_net": "121.95",
659
+ "unit_price_gross": "150.00",
660
+ "net_total": "121.95",
661
+ "vat_amount": "28.05",
662
+ "gross_total": "150.00"
663
+ }
664
+ ],
665
+ "summary": [
666
+ {
667
+ "vat_label": "23%",
668
+ "net_total": "121.95",
669
+ "vat_total": "28.05",
670
+ "gross_total": "150.00"
671
+ }
672
+ ],
673
+ "totals": {
674
+ "net": "121.95",
675
+ "vat": "28.05",
676
+ "gross": "150.00"
677
+ },
678
+ "client": {
679
+ "name": "Piotr Brzoska",
680
+ "address_line": "Grottgera 33",
681
+ "postal_code": "44-105",
682
+ "city": "Gliwice",
683
+ "tax_id": ""
684
+ },
685
+ "exemption_note": ""
686
+ }
687
+ ]
688
+ }