Antoni09 commited on
Commit
007fb1d
·
verified ·
1 Parent(s): 99e804f

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.js +0 -1
  2. server.py +311 -79
main.js CHANGED
@@ -1190,7 +1190,6 @@ function collectInvoicePayload() {
1190
  name,
1191
  quantity,
1192
  unit,
1193
- unitPrice: unitGross.toFixed(2),
1194
  unit_price_gross: unitGross.toFixed(2),
1195
  vat_code: vatCode,
1196
  });
 
1190
  name,
1191
  quantity,
1192
  unit,
 
1193
  unit_price_gross: unitGross.toFixed(2),
1194
  vat_code: vatCode,
1195
  });
server.py CHANGED
@@ -12,16 +12,17 @@ from typing import Any, Dict, List, Optional, Tuple
12
 
13
  from flask import Flask, jsonify, request, send_from_directory
14
 
15
- from db import (
16
- create_account,
17
- fetch_all,
18
- fetch_one,
19
- fetch_business_logo,
20
- insert_invoice,
21
- update_business,
22
- update_business_logo,
23
- upsert_client,
24
- )
 
25
 
26
  APP_ROOT = Path(__file__).parent.resolve()
27
  DATA_DIR = Path(os.environ.get("DATA_DIR", APP_ROOT))
@@ -66,15 +67,26 @@ def _quantize(value: Decimal) -> Decimal:
66
  return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
67
 
68
 
69
- def _decimal(value: Any) -> Decimal:
70
- try:
71
- return Decimal(str(value))
72
- except Exception as error: # pragma: no cover - defensive
73
- raise ValueError(f"Niepoprawna wartosc liczby: {value}") from error
74
-
75
-
76
- def hash_password(password: str) -> str:
77
- return hashlib.sha256(password.encode("utf-8")).hexdigest()
 
 
 
 
 
 
 
 
 
 
 
78
 
79
 
80
  EMAIL_PATTERN = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
@@ -139,11 +151,23 @@ def get_account(data: Dict[str, Any], login_key: str) -> Dict[str, Any]:
139
  return account
140
 
141
 
142
- def get_account_row(login_key: str) -> Dict[str, Any]:
143
- row = fetch_one("SELECT id, login FROM accounts WHERE login = %s", (login_key,))
144
- if not row:
145
- raise KeyError("Nie znaleziono konta.")
146
- return row
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
 
149
  def require_auth() -> str:
@@ -441,44 +465,48 @@ def normalize_phone(phone: Optional[str]) -> Optional[str]:
441
  return digits or None
442
 
443
 
444
- def validate_client(payload: Dict[str, Any]) -> Dict[str, str]:
445
- client = {
446
- "name": (payload.get("clientName") or "").strip(),
447
- "tax_id": (payload.get("clientTaxId") or "").strip(),
448
- "address_line": (payload.get("clientAddress") or "").strip(),
449
- "postal_code": (payload.get("clientPostalCode") or "").strip(),
450
- "city": (payload.get("clientCity") or "").strip(),
451
- "phone": normalize_phone(payload.get("clientPhone")),
452
- }
453
- return client
454
-
455
-
456
- def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dict[str, str]) -> Dict[str, Any]:
457
- now = datetime.now()
458
- invoice_id = f"FV-{now.strftime('%Y%m%d-%H%M%S')}"
459
- issued_at = now.strftime("%Y-%m-%d %H:%M")
460
- sale_date = payload.get("saleDate") or date.today().isoformat()
461
- payment_term = int(payload.get("paymentTerm") or 14)
462
- items = payload.get("items") or []
463
-
464
- normalized_items: List[Dict[str, Any]] = []
465
- for item in items:
466
- name = (item.get("name") or "").strip()
 
467
  if not name:
468
  raise ValueError("Nazwa pozycji nie moze byc pusta.")
469
  quantity = _quantize(_decimal(item.get("quantity") or "0"))
470
  if quantity <= Decimal("0"):
471
  raise ValueError("Ilosc musi byc dodatnia.")
472
  unit = item.get("unit") or DEFAULT_UNIT
473
- vat_code = str(item.get("vat") or "23")
474
- if vat_code not in VAT_RATES:
475
- raise ValueError("Niepoprawna stawka VAT.")
476
- unit_price_gross = _quantize(_decimal(item.get("unitPrice") or "0"))
477
- if unit_price_gross <= Decimal("0"):
478
- raise ValueError("Cena musi byc dodatnia.")
479
- vat_rate = VAT_RATES[vat_code]
480
- if vat_rate is None:
481
- unit_price_net = unit_price_gross
 
 
 
482
  vat_amount = Decimal("0.00")
483
  else:
484
  unit_price_net = _quantize(unit_price_gross / (Decimal("1.0") + vat_rate))
@@ -524,7 +552,7 @@ def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dic
524
  for label, values in summary.items()
525
  ]
526
 
527
- exemption_note = payload.get("exemptionNote", "").strip()
528
 
529
  return {
530
  "invoice_id": invoice_id,
@@ -553,20 +581,127 @@ def api_invoices() -> Any:
553
  account_row = get_account_row(login_key)
554
  except KeyError:
555
  return jsonify({"error": "Nie znaleziono konta."}), 404
556
- rows = fetch_all(
557
  """
558
- SELECT invoice_number AS invoice_id,
559
- to_char(issued_at, 'YYYY-MM-DD HH24:MI') AS issued_at,
560
- sale_date,
561
- total_gross
562
- FROM invoices
563
- WHERE account_id = %s
564
- ORDER BY issued_at DESC
 
 
 
 
 
 
 
 
 
 
 
 
565
  LIMIT %s
566
  """,
567
  (account_row["id"], INVOICE_HISTORY_LIMIT),
568
  )
569
- return jsonify({"invoices": rows})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
  data = load_store()
572
  try:
@@ -583,15 +718,7 @@ def api_invoices() -> Any:
583
  account_row = get_account_row(login_key)
584
  except KeyError:
585
  return jsonify({"error": "Nie znaleziono konta."}), 404
586
- business = fetch_one(
587
- """
588
- SELECT company_name, owner_name, address_line, postal_code,
589
- city, tax_id, bank_account
590
- FROM business_profiles
591
- WHERE account_id = %s
592
- """,
593
- (account_row["id"],),
594
- )
595
  if not business:
596
  return jsonify({"error": "Ustaw dane sprzedawcy przed dodaniem faktury."}), 400
597
 
@@ -636,10 +763,115 @@ def api_invoices() -> Any:
636
  account["invoices"] = invoices[:INVOICE_HISTORY_LIMIT]
637
  save_store(data)
638
  return jsonify({"message": "Faktura zostala zapisana.", "invoice": invoice})
639
-
640
-
641
- @app.route("/api/invoices/summary", methods=["GET"])
642
- def api_invoice_summary() -> Any:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  try:
644
  login_key = require_auth()
645
  except PermissionError:
 
12
 
13
  from flask import Flask, jsonify, request, send_from_directory
14
 
15
+ from db import (
16
+ create_account,
17
+ execute,
18
+ fetch_all,
19
+ fetch_one,
20
+ fetch_business_logo,
21
+ insert_invoice,
22
+ update_business,
23
+ update_business_logo,
24
+ upsert_client,
25
+ )
26
 
27
  APP_ROOT = Path(__file__).parent.resolve()
28
  DATA_DIR = Path(os.environ.get("DATA_DIR", APP_ROOT))
 
67
  return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
68
 
69
 
70
+ def _decimal(value: Any) -> Decimal:
71
+ try:
72
+ return Decimal(str(value))
73
+ except Exception as error: # pragma: no cover - defensive
74
+ raise ValueError(f"Niepoprawna wartosc liczby: {value}") from error
75
+
76
+
77
+ def _format_decimal_str(value: Any, default: str = "0.00") -> str:
78
+ if value in (None, ""):
79
+ return default
80
+ if isinstance(value, Decimal):
81
+ return str(_quantize(value))
82
+ try:
83
+ return str(_quantize(Decimal(str(value))))
84
+ except Exception:
85
+ return str(value)
86
+
87
+
88
+ def hash_password(password: str) -> str:
89
+ return hashlib.sha256(password.encode("utf-8")).hexdigest()
90
 
91
 
92
  EMAIL_PATTERN = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
 
151
  return account
152
 
153
 
154
+ def get_account_row(login_key: str) -> Dict[str, Any]:
155
+ row = fetch_one("SELECT id, login FROM accounts WHERE login = %s", (login_key,))
156
+ if not row:
157
+ raise KeyError("Nie znaleziono konta.")
158
+ return row
159
+
160
+
161
+ def get_business_profile(account_id: int) -> Optional[Dict[str, Any]]:
162
+ return fetch_one(
163
+ """
164
+ SELECT company_name, owner_name, address_line, postal_code,
165
+ city, tax_id, bank_account
166
+ FROM business_profiles
167
+ WHERE account_id = %s
168
+ """,
169
+ (account_id,),
170
+ )
171
 
172
 
173
  def require_auth() -> str:
 
465
  return digits or None
466
 
467
 
468
+ def validate_client(payload: Dict[str, Any]) -> Dict[str, str]:
469
+ client_payload = payload.get("client") or {}
470
+ client = {
471
+ "name": (client_payload.get("name") or payload.get("clientName") or "").strip(),
472
+ "tax_id": (client_payload.get("tax_id") or payload.get("clientTaxId") or "").strip(),
473
+ "address_line": (client_payload.get("address_line") or payload.get("clientAddress") or "").strip(),
474
+ "postal_code": (client_payload.get("postal_code") or payload.get("clientPostalCode") or "").strip(),
475
+ "city": (client_payload.get("city") or payload.get("clientCity") or "").strip(),
476
+ "phone": normalize_phone(client_payload.get("phone") or payload.get("clientPhone")),
477
+ }
478
+ return client
479
+
480
+
481
+ def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dict[str, str]) -> Dict[str, Any]:
482
+ now = datetime.now()
483
+ invoice_id = f"FV-{now.strftime('%Y%m%d-%H%M%S')}"
484
+ issued_at = now.strftime("%Y-%m-%d %H:%M")
485
+ sale_date = payload.get("sale_date") or payload.get("saleDate") or date.today().isoformat()
486
+ payment_term = int(payload.get("payment_term") or payload.get("paymentTerm") or 14)
487
+ items = payload.get("items") or []
488
+
489
+ normalized_items: List[Dict[str, Any]] = []
490
+ for item in items:
491
+ name = (item.get("name") or "").strip()
492
  if not name:
493
  raise ValueError("Nazwa pozycji nie moze byc pusta.")
494
  quantity = _quantize(_decimal(item.get("quantity") or "0"))
495
  if quantity <= Decimal("0"):
496
  raise ValueError("Ilosc musi byc dodatnia.")
497
  unit = item.get("unit") or DEFAULT_UNIT
498
+ vat_code = str(item.get("vat_code") or item.get("vat") or item.get("vatCode") or "23")
499
+ if vat_code not in VAT_RATES:
500
+ raise ValueError("Niepoprawna stawka VAT.")
501
+ unit_price_raw = item.get("unit_price_gross")
502
+ if unit_price_raw in (None, ""):
503
+ unit_price_raw = item.get("unitPrice") or item.get("unit_price") or item.get("price")
504
+ unit_price_gross = _quantize(_decimal(unit_price_raw or "0"))
505
+ if unit_price_gross <= Decimal("0"):
506
+ raise ValueError("Cena musi byc dodatnia.")
507
+ vat_rate = VAT_RATES[vat_code]
508
+ if vat_rate is None:
509
+ unit_price_net = unit_price_gross
510
  vat_amount = Decimal("0.00")
511
  else:
512
  unit_price_net = _quantize(unit_price_gross / (Decimal("1.0") + vat_rate))
 
552
  for label, values in summary.items()
553
  ]
554
 
555
+ exemption_note = (payload.get("exemption_note") or payload.get("exemptionNote") or "").strip()
556
 
557
  return {
558
  "invoice_id": invoice_id,
 
581
  account_row = get_account_row(login_key)
582
  except KeyError:
583
  return jsonify({"error": "Nie znaleziono konta."}), 404
584
+ invoice_rows = fetch_all(
585
  """
586
+ SELECT i.id,
587
+ i.invoice_number,
588
+ i.issued_at,
589
+ i.sale_date,
590
+ i.payment_term_days,
591
+ i.exemption_note,
592
+ i.total_net,
593
+ i.total_vat,
594
+ i.total_gross,
595
+ c.name AS client_name,
596
+ c.address_line AS client_address,
597
+ c.postal_code AS client_postal_code,
598
+ c.city AS client_city,
599
+ c.tax_id AS client_tax_id,
600
+ c.phone AS client_phone
601
+ FROM invoices AS i
602
+ LEFT JOIN clients AS c ON c.id = i.client_id
603
+ WHERE i.account_id = %s
604
+ ORDER BY i.issued_at DESC
605
  LIMIT %s
606
  """,
607
  (account_row["id"], INVOICE_HISTORY_LIMIT),
608
  )
609
+ if not invoice_rows:
610
+ return jsonify({"invoices": []})
611
+
612
+ invoice_ids = [row["id"] for row in invoice_rows]
613
+ items_map: Dict[int, List[Dict[str, Any]]] = {row_id: [] for row_id in invoice_ids}
614
+ summary_map: Dict[int, List[Dict[str, str]]] = {row_id: [] for row_id in invoice_ids}
615
+
616
+ if invoice_ids:
617
+ item_rows = fetch_all(
618
+ """
619
+ SELECT invoice_id, line_no, name, quantity, unit,
620
+ vat_code, vat_label, unit_price_net,
621
+ unit_price_gross, net_total, vat_amount, gross_total
622
+ FROM invoice_items
623
+ WHERE invoice_id = ANY(%s)
624
+ ORDER BY line_no
625
+ """,
626
+ (invoice_ids,),
627
+ )
628
+ for item in item_rows:
629
+ items_map.setdefault(item["invoice_id"], []).append(
630
+ {
631
+ "name": item["name"],
632
+ "quantity": _format_decimal_str(item.get("quantity"), "0.00"),
633
+ "unit": item.get("unit") or DEFAULT_UNIT,
634
+ "vat_code": item.get("vat_code"),
635
+ "vat_label": item.get("vat_label") or item.get("vat_code"),
636
+ "unit_price_net": _format_decimal_str(item.get("unit_price_net")),
637
+ "unit_price_gross": _format_decimal_str(item.get("unit_price_gross")),
638
+ "net_total": _format_decimal_str(item.get("net_total")),
639
+ "vat_amount": _format_decimal_str(item.get("vat_amount")),
640
+ "gross_total": _format_decimal_str(item.get("gross_total")),
641
+ }
642
+ )
643
+
644
+ summary_rows = fetch_all(
645
+ """
646
+ SELECT invoice_id, vat_label, net_total, vat_total, gross_total
647
+ FROM invoice_vat_summary
648
+ WHERE invoice_id = ANY(%s)
649
+ ORDER BY vat_label
650
+ """,
651
+ (invoice_ids,),
652
+ )
653
+ for entry in summary_rows:
654
+ summary_map.setdefault(entry["invoice_id"], []).append(
655
+ {
656
+ "vat_label": entry.get("vat_label"),
657
+ "net_total": _format_decimal_str(entry.get("net_total")),
658
+ "vat_total": _format_decimal_str(entry.get("vat_total")),
659
+ "gross_total": _format_decimal_str(entry.get("gross_total")),
660
+ }
661
+ )
662
+
663
+ business_profile = get_business_profile(account_row["id"])
664
+ invoices: List[Dict[str, Any]] = []
665
+ for row in invoice_rows:
666
+ issued_at_value = row.get("issued_at")
667
+ sale_date_value = row.get("sale_date")
668
+ if isinstance(issued_at_value, datetime):
669
+ issued_at = issued_at_value.strftime("%Y-%m-%d %H:%M")
670
+ else:
671
+ issued_at = issued_at_value
672
+ if hasattr(sale_date_value, "isoformat"):
673
+ sale_date = sale_date_value.isoformat()
674
+ else:
675
+ sale_date = sale_date_value
676
+ client = None
677
+ if row.get("client_name"):
678
+ client = {
679
+ "name": row.get("client_name"),
680
+ "address_line": row.get("client_address"),
681
+ "postal_code": row.get("client_postal_code"),
682
+ "city": row.get("client_city"),
683
+ "tax_id": row.get("client_tax_id"),
684
+ "phone": row.get("client_phone"),
685
+ }
686
+ invoices.append(
687
+ {
688
+ "invoice_id": row.get("invoice_number"),
689
+ "issued_at": issued_at,
690
+ "sale_date": sale_date,
691
+ "payment_term": row.get("payment_term_days"),
692
+ "exemption_note": row.get("exemption_note"),
693
+ "items": items_map.get(row["id"], []),
694
+ "summary": summary_map.get(row["id"], []),
695
+ "totals": {
696
+ "net": _format_decimal_str(row.get("total_net")),
697
+ "vat": _format_decimal_str(row.get("total_vat")),
698
+ "gross": _format_decimal_str(row.get("total_gross")),
699
+ },
700
+ "client": client,
701
+ "business": business_profile,
702
+ }
703
+ )
704
+ return jsonify({"invoices": invoices})
705
 
706
  data = load_store()
707
  try:
 
718
  account_row = get_account_row(login_key)
719
  except KeyError:
720
  return jsonify({"error": "Nie znaleziono konta."}), 404
721
+ business = get_business_profile(account_row["id"])
 
 
 
 
 
 
 
 
722
  if not business:
723
  return jsonify({"error": "Ustaw dane sprzedawcy przed dodaniem faktury."}), 400
724
 
 
763
  account["invoices"] = invoices[:INVOICE_HISTORY_LIMIT]
764
  save_store(data)
765
  return jsonify({"message": "Faktura zostala zapisana.", "invoice": invoice})
766
+
767
+
768
+ @app.route("/api/invoices/<invoice_id>", methods=["PUT", "DELETE"])
769
+ def api_invoice_detail(invoice_id: str) -> Any:
770
+ try:
771
+ login_key = require_auth()
772
+ except PermissionError:
773
+ return jsonify({"error": "Brak autoryzacji."}), 401
774
+
775
+ if DATABASE_AVAILABLE:
776
+ try:
777
+ account_row = get_account_row(login_key)
778
+ except KeyError:
779
+ return jsonify({"error": "Nie znaleziono konta."}), 404
780
+
781
+ invoice_row = fetch_one(
782
+ """
783
+ SELECT id, issued_at
784
+ FROM invoices
785
+ WHERE account_id = %s AND invoice_number = %s
786
+ """,
787
+ (account_row["id"], invoice_id),
788
+ )
789
+ if not invoice_row:
790
+ return jsonify({"error": "Nie znaleziono faktury."}), 404
791
+
792
+ if request.method == "DELETE":
793
+ execute("DELETE FROM invoice_items WHERE invoice_id = %s", (invoice_row["id"],))
794
+ execute("DELETE FROM invoice_vat_summary WHERE invoice_id = %s", (invoice_row["id"],))
795
+ execute("DELETE FROM invoices WHERE id = %s", (invoice_row["id"],))
796
+ return jsonify({"message": "Faktura zostala usunieta."})
797
+
798
+ payload = request.get_json(force=True)
799
+ business = get_business_profile(account_row["id"])
800
+ if not business:
801
+ return jsonify({"error": "Ustaw dane sprzedawcy przed dodaniem faktury."}), 400
802
+
803
+ client = validate_client(payload)
804
+ try:
805
+ invoice = build_invoice(payload, business, client)
806
+ except ValueError as error:
807
+ return jsonify({"error": str(error)}), 400
808
+
809
+ invoice["invoice_id"] = invoice_id
810
+ existing_issued_at = invoice_row.get("issued_at")
811
+ if isinstance(existing_issued_at, datetime):
812
+ invoice["issued_at"] = existing_issued_at.strftime("%Y-%m-%d %H:%M")
813
+ elif existing_issued_at:
814
+ invoice["issued_at"] = str(existing_issued_at)
815
+
816
+ client_id = upsert_client(
817
+ account_row["id"],
818
+ {
819
+ "name": client["name"],
820
+ "address_line": client["address_line"],
821
+ "postal_code": client["postal_code"],
822
+ "city": client["city"],
823
+ "tax_id": client["tax_id"],
824
+ "phone": client.get("phone"),
825
+ },
826
+ )
827
+
828
+ execute("DELETE FROM invoice_items WHERE invoice_id = %s", (invoice_row["id"],))
829
+ execute("DELETE FROM invoice_vat_summary WHERE invoice_id = %s", (invoice_row["id"],))
830
+ execute("DELETE FROM invoices WHERE id = %s", (invoice_row["id"],))
831
+ insert_invoice(account_row["id"], client_id, invoice)
832
+ return jsonify({"message": "Faktura zostala zaktualizowana.", "invoice": invoice})
833
+
834
+ data = load_store()
835
+ try:
836
+ account = get_account(data, login_key)
837
+ except KeyError:
838
+ return jsonify({"error": "Nie znaleziono konta."}), 404
839
+
840
+ invoices = account.get("invoices", [])
841
+ invoice_index = next(
842
+ (idx for idx, entry in enumerate(invoices) if entry.get("invoice_id") == invoice_id),
843
+ None,
844
+ )
845
+ if invoice_index is None:
846
+ return jsonify({"error": "Nie znaleziono faktury."}), 404
847
+
848
+ if request.method == "DELETE":
849
+ invoices.pop(invoice_index)
850
+ save_store(data)
851
+ return jsonify({"message": "Faktura zostala usunieta."})
852
+
853
+ payload = request.get_json(force=True)
854
+ business = account.get("business")
855
+ if not business:
856
+ return jsonify({"error": "Ustaw dane sprzedawcy przed dodaniem faktury."}), 400
857
+
858
+ client = validate_client(payload)
859
+ try:
860
+ invoice = build_invoice(payload, business, client)
861
+ except ValueError as error:
862
+ return jsonify({"error": str(error)}), 400
863
+
864
+ invoice["invoice_id"] = invoice_id
865
+ existing_invoice = invoices[invoice_index]
866
+ if existing_invoice.get("issued_at"):
867
+ invoice["issued_at"] = existing_invoice.get("issued_at")
868
+ invoices[invoice_index] = invoice
869
+ save_store(data)
870
+ return jsonify({"message": "Faktura zostala zaktualizowana.", "invoice": invoice})
871
+
872
+
873
+ @app.route("/api/invoices/summary", methods=["GET"])
874
+ def api_invoice_summary() -> Any:
875
  try:
876
  login_key = require_auth()
877
  except PermissionError: