Antoni09 commited on
Commit
e69ba0c
·
1 Parent(s): 40beee2

Add paid personal invoice mode

Browse files
Files changed (4) hide show
  1. db.py +41 -38
  2. index.html +9 -1
  3. main.js +144 -64
  4. server.py +37 -10
db.py CHANGED
@@ -124,38 +124,41 @@ def update_business_logo(account_id: int, mime: Optional[str], data_base64: Opti
124
  )
125
 
126
  def upsert_client(account_id: int, payload: Dict[str, str]) -> int:
127
- row = fetch_one(
128
- """
129
- SELECT id FROM clients
130
- WHERE account_id = %s AND tax_id = %s
131
- """,
132
- (account_id, payload["tax_id"]),
133
- )
134
- if row:
135
- client_id = row["id"]
136
- execute(
137
- """
138
- UPDATE clients
139
- SET name = %s,
140
- address_line = %s,
141
- postal_code = %s,
142
- city = %s,
143
- phone = %s
144
- WHERE id = %s
145
- """,
146
- (
147
- payload["name"],
148
- payload["address_line"],
149
- payload["postal_code"],
150
- payload["city"],
151
- payload.get("phone"),
152
- client_id,
153
- ),
154
- )
155
- return client_id
156
-
157
- with db_conn() as conn, conn.cursor() as cur:
158
- cur.execute(
 
 
 
159
  """
160
  INSERT INTO clients (account_id, name, address_line, postal_code, city, tax_id, phone)
161
  VALUES (%s, %s, %s, %s, %s, %s, %s)
@@ -164,12 +167,12 @@ def upsert_client(account_id: int, payload: Dict[str, str]) -> int:
164
  (
165
  account_id,
166
  payload["name"],
167
- payload["address_line"],
168
- payload["postal_code"],
169
- payload["city"],
170
- payload["tax_id"],
171
- payload.get("phone"),
172
- ),
173
  )
174
  return cur.fetchone()["id"]
175
 
 
124
  )
125
 
126
  def upsert_client(account_id: int, payload: Dict[str, str]) -> int:
127
+ tax_id = (payload.get("tax_id") or "").strip()
128
+ stored_tax_id = tax_id or None
129
+ if tax_id:
130
+ row = fetch_one(
131
+ """
132
+ SELECT id FROM clients
133
+ WHERE account_id = %s AND tax_id = %s
134
+ """,
135
+ (account_id, tax_id),
136
+ )
137
+ if row:
138
+ client_id = row["id"]
139
+ execute(
140
+ """
141
+ UPDATE clients
142
+ SET name = %s,
143
+ address_line = %s,
144
+ postal_code = %s,
145
+ city = %s,
146
+ phone = %s
147
+ WHERE id = %s
148
+ """,
149
+ (
150
+ payload["name"],
151
+ payload["address_line"],
152
+ payload["postal_code"],
153
+ payload["city"],
154
+ payload.get("phone"),
155
+ client_id,
156
+ ),
157
+ )
158
+ return client_id
159
+
160
+ with db_conn() as conn, conn.cursor() as cur:
161
+ cur.execute(
162
  """
163
  INSERT INTO clients (account_id, name, address_line, postal_code, city, tax_id, phone)
164
  VALUES (%s, %s, %s, %s, %s, %s, %s)
 
167
  (
168
  account_id,
169
  payload["name"],
170
+ payload["address_line"],
171
+ payload["postal_code"],
172
+ payload["city"],
173
+ stored_tax_id,
174
+ payload.get("phone"),
175
+ ),
176
  )
177
  return cur.fetchone()["id"]
178
 
index.html CHANGED
@@ -223,15 +223,23 @@
223
  <fieldset>
224
  <legend>Informacje o fakturze</legend>
225
  <div class="field-grid">
 
 
 
 
 
 
 
226
  <label>
227
  Data sprzedaży / wykonania usługi
228
  <input type="date" name="saleDate">
229
  </label>
230
- <label>
231
  Termin płatności (dni)
232
  <input type="number" name="paymentTerm" min="1" step="1" value="14">
233
  </label>
234
  </div>
 
235
  </fieldset>
236
 
237
  <fieldset>
 
223
  <fieldset>
224
  <legend>Informacje o fakturze</legend>
225
  <div class="field-grid">
226
+ <label>
227
+ Typ dokumentu
228
+ <select name="documentType" id="document-type">
229
+ <option value="standard">Faktura VAT</option>
230
+ <option value="personal_paid">Faktura imienna - zapłacona</option>
231
+ </select>
232
+ </label>
233
  <label>
234
  Data sprzedaży / wykonania usługi
235
  <input type="date" name="saleDate">
236
  </label>
237
+ <label id="payment-term-field">
238
  Termin płatności (dni)
239
  <input type="number" name="paymentTerm" min="1" step="1" value="14">
240
  </label>
241
  </div>
242
+ <p id="paid-document-hint" class="hint hidden">Ten dokument będzie oznaczony jako opłacony i może służyć do sprzedaży imiennej zamiast paragonu.</p>
243
  </fieldset>
244
 
245
  <fieldset>
main.js CHANGED
@@ -26,7 +26,20 @@ const UNIT_OPTIONS = [
26
  { value: "godz.", label: "godz." },
27
  ];
28
 
29
- const DEFAULT_UNIT = UNIT_OPTIONS[0].value;
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  const EXEMPTION_REASONS = [
32
  {
@@ -92,9 +105,12 @@ const cancelBusinessUpdateButton = document.getElementById("cancel-business-upda
92
  const currentLoginLabel = document.getElementById("current-login-label");
93
 
94
  const itemsBody = document.getElementById("items-body");
95
- const addItemButton = document.getElementById("add-item-button");
96
-
97
- const totalNetLabel = document.getElementById("total-net");
 
 
 
98
  const totalVatLabel = document.getElementById("total-vat");
99
  const totalGrossLabel = document.getElementById("total-gross");
100
  const rateSummaryContainer = document.getElementById("rate-summary");
@@ -288,12 +304,51 @@ function formatQuantity(value) {
288
  return parsed.toString();
289
  }
290
 
291
- function formatCurrency(value) {
292
- const number = parseNumber(value);
293
- return `${number.toFixed(2)} PLN`;
294
- }
295
-
296
- function vatLabelFromCode(code) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  if (code === "ZW" || code === "0") {
298
  return "ZW";
299
  }
@@ -508,10 +563,10 @@ function renderInvoicesTable(invoices) {
508
  invoicesEmpty.classList.add("hidden");
509
  invoices.forEach((invoice) => {
510
  const row = document.createElement("tr");
511
-
512
- const numberCell = document.createElement("td");
513
- numberCell.textContent = invoice.invoice_id || "---";
514
- row.appendChild(numberCell);
515
 
516
  const issuedCell = document.createElement("td");
517
  issuedCell.textContent = invoice.issued_at || "-";
@@ -797,10 +852,14 @@ function startInvoiceEdit(invoiceId) {
797
  saveInvoiceButton.textContent = "Zapisz zmiany";
798
  cancelEditInvoiceButton.classList.remove("hidden");
799
  setActiveView("invoice-builder");
800
-
801
- resetInvoiceForm();
802
- invoiceForm.elements.saleDate.value = invoice.sale_date || "";
803
- invoiceForm.elements.paymentTerm.value = invoice.payment_term || 14;
 
 
 
 
804
 
805
  if (invoice.client) {
806
  setClientFormValues(invoice.client);
@@ -1333,13 +1392,14 @@ function collectInvoicePayload() {
1333
  });
1334
  });
1335
 
1336
- if (items.length === 0) {
1337
- throw new Error("Dodaj przynajmniej jedną pozycję.");
1338
- }
1339
-
1340
- const saleDate = invoiceForm.elements.saleDate.value || null;
1341
- const paymentTerm = parseInt(invoiceForm.elements.paymentTerm.value) || 14;
1342
- const requiresExemptionNote = items.some((item) => item.vat_code === "ZW" || item.vat_code === "0");
 
1343
  let exemptionNote = "";
1344
  if (requiresExemptionNote) {
1345
  const noteFromTextarea = exemptionNoteInput.value.trim();
@@ -1357,18 +1417,24 @@ function collectInvoicePayload() {
1357
  throw new Error("Wybierz lub wpisz podstawę zwolnienia dla pozycji ze stawka ZW/0%.");
1358
  }
1359
  }
1360
- const client = {
1361
- name: (invoiceForm.elements.clientName.value || "").trim(),
1362
- tax_id: (invoiceForm.elements.clientTaxId.value || "").trim(),
1363
- address_line: (invoiceForm.elements.clientAddress.value || "").trim(),
1364
- postal_code: (invoiceForm.elements.clientPostalCode.value || "").trim(),
1365
- city: (invoiceForm.elements.clientCity.value || "").trim(),
1366
- phone: (invoiceForm.elements.clientPhone.value || "").trim(),
1367
- };
1368
-
1369
- return {
1370
- sale_date: saleDate,
1371
- payment_term: paymentTerm,
 
 
 
 
 
 
1372
  client,
1373
  items,
1374
  exemption_note: exemptionNote,
@@ -1380,11 +1446,12 @@ function renderInvoicePreview(invoice) {
1380
  invoiceOutput.innerHTML = "<p>Brak danych faktury.</p>";
1381
  return;
1382
  }
1383
-
1384
- const client = invoice.client || {};
1385
- const hasClientData = client.name || client.address_line || client.postal_code || client.city || client.tax_id;
1386
-
1387
- const itemsRows = (invoice.items || [])
 
1388
  .map((item) => {
1389
  const quantityDisplay = formatQuantity(item.quantity);
1390
  const unitDisplay = UNIT_OPTIONS.some((option) => option.value === item.unit) ? item.unit : DEFAULT_UNIT;
@@ -1414,13 +1481,15 @@ function renderInvoicePreview(invoice) {
1414
  )
1415
  .join("");
1416
 
1417
- invoiceOutput.innerHTML = `
1418
- <div class="invoice-preview-meta">
1419
- <span><strong>Numer:</strong> ${invoice.invoice_id}</span>
1420
- <span><strong>Data wystawienia:</strong> ${invoice.issued_at}</span>
1421
- <span><strong>Data sprzedaży:</strong> ${invoice.sale_date}</span>
1422
- ${invoice.payment_term ? `<span><strong>Termin płatności:</strong> ${invoice.payment_term} dni</span>` : ''}
1423
- </div>
 
 
1424
  <div class="invoice-preview-header">
1425
  <div class="invoice-preview-card">
1426
  <h4>Nabywca</h4>
@@ -1587,7 +1656,7 @@ async function generatePdf(business, invoice, logo) {
1587
  doc.setFont(PDF_FONT_NAME, "normal");
1588
  doc.setTextColor(...PDF_COLORS.text);
1589
  doc.setFontSize(18);
1590
- doc.text("Faktura", marginX, cursorY + 2);
1591
  doc.setFontSize(13);
1592
  doc.text(invoice.invoice_id, marginX, cursorY + 10);
1593
  doc.setFontSize(10);
@@ -1596,7 +1665,9 @@ async function generatePdf(business, invoice, logo) {
1596
  `Data wystawienia: ${invoice.issued_at}`,
1597
  `Data sprzedaży: ${invoice.sale_date}`,
1598
  ];
1599
- if (invoice.payment_term) {
 
 
1600
  metaLines.push(`Termin płatności: ${invoice.payment_term} dni`);
1601
  }
1602
  metaLines.forEach((line, index) => {
@@ -1851,10 +1922,14 @@ async function loadLogo() {
1851
  updateLogoPreview();
1852
  }
1853
 
1854
- function resetInvoiceForm() {
1855
- invoiceForm.reset();
1856
- customExemptionNote = "";
1857
- updateExemptionVisibility(false);
 
 
 
 
1858
  itemsBody.innerHTML = "";
1859
  createItemRow();
1860
  const now = new Date();
@@ -2101,15 +2176,20 @@ if (exemptionReasonSelect) {
2101
  });
2102
  }
2103
 
2104
- if (exemptionNoteInput) {
2105
- exemptionNoteInput.addEventListener("input", () => {
2106
- if (exemptionReasonSelect && exemptionReasonSelect.value === "custom") {
2107
- customExemptionNote = exemptionNoteInput.value;
2108
- }
2109
- });
2110
- }
2111
-
2112
- if (invoiceForm) {
 
 
 
 
 
2113
  invoiceForm.addEventListener("submit", async (event) => {
2114
  event.preventDefault();
2115
  try {
 
26
  { value: "godz.", label: "godz." },
27
  ];
28
 
29
+ const DEFAULT_UNIT = UNIT_OPTIONS[0].value;
30
+
31
+ const DOCUMENT_TYPES = {
32
+ standard: {
33
+ label: "Faktura VAT",
34
+ pdfTitle: "Faktura",
35
+ prefix: "FV",
36
+ },
37
+ personal_paid: {
38
+ label: "Faktura imienna",
39
+ pdfTitle: "Faktura imienna",
40
+ prefix: "FI",
41
+ },
42
+ };
43
 
44
  const EXEMPTION_REASONS = [
45
  {
 
105
  const currentLoginLabel = document.getElementById("current-login-label");
106
 
107
  const itemsBody = document.getElementById("items-body");
108
+ const addItemButton = document.getElementById("add-item-button");
109
+ const documentTypeSelect = document.getElementById("document-type");
110
+ const paymentTermField = document.getElementById("payment-term-field");
111
+ const paidDocumentHint = document.getElementById("paid-document-hint");
112
+
113
+ const totalNetLabel = document.getElementById("total-net");
114
  const totalVatLabel = document.getElementById("total-vat");
115
  const totalGrossLabel = document.getElementById("total-gross");
116
  const rateSummaryContainer = document.getElementById("rate-summary");
 
304
  return parsed.toString();
305
  }
306
 
307
+ function formatCurrency(value) {
308
+ const number = parseNumber(value);
309
+ return `${number.toFixed(2)} PLN`;
310
+ }
311
+
312
+ function getDocumentType(invoice = {}) {
313
+ const rawType = invoice.document_type || invoice.documentType || "";
314
+ if (DOCUMENT_TYPES[rawType]) {
315
+ return rawType;
316
+ }
317
+ if (invoice.payment_status === "paid" || (invoice.payment_term !== null && invoice.payment_term !== undefined && Number(invoice.payment_term) === 0)) {
318
+ return "personal_paid";
319
+ }
320
+ const number = invoice.invoice_id || "";
321
+ if (number.startsWith(`${DOCUMENT_TYPES.personal_paid.prefix}-`)) {
322
+ return "personal_paid";
323
+ }
324
+ return "standard";
325
+ }
326
+
327
+ function getDocumentLabel(invoice = {}) {
328
+ return DOCUMENT_TYPES[getDocumentType(invoice)]?.label || DOCUMENT_TYPES.standard.label;
329
+ }
330
+
331
+ function isPaidInvoice(invoice = {}) {
332
+ return (
333
+ invoice.payment_status === "paid" ||
334
+ getDocumentType(invoice) === "personal_paid" ||
335
+ (invoice.payment_term !== null && invoice.payment_term !== undefined && Number(invoice.payment_term) === 0)
336
+ );
337
+ }
338
+
339
+ function syncDocumentTypeControls() {
340
+ const selectedType = documentTypeSelect?.value || "standard";
341
+ const isPersonalPaid = selectedType === "personal_paid";
342
+ setVisibility(paymentTermField, !isPersonalPaid);
343
+ setVisibility(paidDocumentHint, isPersonalPaid);
344
+ if (isPersonalPaid && invoiceForm?.elements.paymentTerm) {
345
+ invoiceForm.elements.paymentTerm.value = "0";
346
+ } else if (invoiceForm?.elements.paymentTerm && !invoiceForm.elements.paymentTerm.value) {
347
+ invoiceForm.elements.paymentTerm.value = "14";
348
+ }
349
+ }
350
+
351
+ function vatLabelFromCode(code) {
352
  if (code === "ZW" || code === "0") {
353
  return "ZW";
354
  }
 
563
  invoicesEmpty.classList.add("hidden");
564
  invoices.forEach((invoice) => {
565
  const row = document.createElement("tr");
566
+
567
+ const numberCell = document.createElement("td");
568
+ numberCell.textContent = `${invoice.invoice_id || "---"} (${getDocumentLabel(invoice)})`;
569
+ row.appendChild(numberCell);
570
 
571
  const issuedCell = document.createElement("td");
572
  issuedCell.textContent = invoice.issued_at || "-";
 
852
  saveInvoiceButton.textContent = "Zapisz zmiany";
853
  cancelEditInvoiceButton.classList.remove("hidden");
854
  setActiveView("invoice-builder");
855
+
856
+ resetInvoiceForm();
857
+ if (invoiceForm.elements.documentType) {
858
+ invoiceForm.elements.documentType.value = getDocumentType(invoice);
859
+ syncDocumentTypeControls();
860
+ }
861
+ invoiceForm.elements.saleDate.value = invoice.sale_date || "";
862
+ invoiceForm.elements.paymentTerm.value = isPaidInvoice(invoice) ? 0 : (invoice.payment_term || 14);
863
 
864
  if (invoice.client) {
865
  setClientFormValues(invoice.client);
 
1392
  });
1393
  });
1394
 
1395
+ if (items.length === 0) {
1396
+ throw new Error("Dodaj przynajmniej jedną pozycję.");
1397
+ }
1398
+
1399
+ const documentType = documentTypeSelect?.value === "personal_paid" ? "personal_paid" : "standard";
1400
+ const saleDate = invoiceForm.elements.saleDate.value || null;
1401
+ const paymentTerm = documentType === "personal_paid" ? 0 : (parseInt(invoiceForm.elements.paymentTerm.value) || 14);
1402
+ const requiresExemptionNote = items.some((item) => item.vat_code === "ZW" || item.vat_code === "0");
1403
  let exemptionNote = "";
1404
  if (requiresExemptionNote) {
1405
  const noteFromTextarea = exemptionNoteInput.value.trim();
 
1417
  throw new Error("Wybierz lub wpisz podstawę zwolnienia dla pozycji ze stawka ZW/0%.");
1418
  }
1419
  }
1420
+ const client = {
1421
+ name: (invoiceForm.elements.clientName.value || "").trim(),
1422
+ tax_id: (invoiceForm.elements.clientTaxId.value || "").trim(),
1423
+ address_line: (invoiceForm.elements.clientAddress.value || "").trim(),
1424
+ postal_code: (invoiceForm.elements.clientPostalCode.value || "").trim(),
1425
+ city: (invoiceForm.elements.clientCity.value || "").trim(),
1426
+ phone: (invoiceForm.elements.clientPhone.value || "").trim(),
1427
+ };
1428
+
1429
+ if (documentType === "personal_paid" && !client.name) {
1430
+ throw new Error("Podaj imię i nazwisko nabywcy dla faktury imiennej.");
1431
+ }
1432
+
1433
+ return {
1434
+ document_type: documentType,
1435
+ payment_status: documentType === "personal_paid" ? "paid" : "unpaid",
1436
+ sale_date: saleDate,
1437
+ payment_term: paymentTerm,
1438
  client,
1439
  items,
1440
  exemption_note: exemptionNote,
 
1446
  invoiceOutput.innerHTML = "<p>Brak danych faktury.</p>";
1447
  return;
1448
  }
1449
+
1450
+ const client = invoice.client || {};
1451
+ const hasClientData = client.name || client.address_line || client.postal_code || client.city || client.tax_id;
1452
+ const paid = isPaidInvoice(invoice);
1453
+
1454
+ const itemsRows = (invoice.items || [])
1455
  .map((item) => {
1456
  const quantityDisplay = formatQuantity(item.quantity);
1457
  const unitDisplay = UNIT_OPTIONS.some((option) => option.value === item.unit) ? item.unit : DEFAULT_UNIT;
 
1481
  )
1482
  .join("");
1483
 
1484
+ invoiceOutput.innerHTML = `
1485
+ <div class="invoice-preview-meta">
1486
+ <span><strong>Dokument:</strong> ${getDocumentLabel(invoice)}</span>
1487
+ <span><strong>Numer:</strong> ${invoice.invoice_id}</span>
1488
+ <span><strong>Data wystawienia:</strong> ${invoice.issued_at}</span>
1489
+ <span><strong>Data sprzedaży:</strong> ${invoice.sale_date}</span>
1490
+ ${paid ? `<span><strong>Status płatności:</strong> Zapłacono</span>` : ''}
1491
+ ${!paid && invoice.payment_term ? `<span><strong>Termin płatności:</strong> ${invoice.payment_term} dni</span>` : ''}
1492
+ </div>
1493
  <div class="invoice-preview-header">
1494
  <div class="invoice-preview-card">
1495
  <h4>Nabywca</h4>
 
1656
  doc.setFont(PDF_FONT_NAME, "normal");
1657
  doc.setTextColor(...PDF_COLORS.text);
1658
  doc.setFontSize(18);
1659
+ doc.text(DOCUMENT_TYPES[getDocumentType(invoice)]?.pdfTitle || "Faktura", marginX, cursorY + 2);
1660
  doc.setFontSize(13);
1661
  doc.text(invoice.invoice_id, marginX, cursorY + 10);
1662
  doc.setFontSize(10);
 
1665
  `Data wystawienia: ${invoice.issued_at}`,
1666
  `Data sprzedaży: ${invoice.sale_date}`,
1667
  ];
1668
+ if (isPaidInvoice(invoice)) {
1669
+ metaLines.push("Status płatności: zapłacono");
1670
+ } else if (invoice.payment_term) {
1671
  metaLines.push(`Termin płatności: ${invoice.payment_term} dni`);
1672
  }
1673
  metaLines.forEach((line, index) => {
 
1922
  updateLogoPreview();
1923
  }
1924
 
1925
+ function resetInvoiceForm() {
1926
+ invoiceForm.reset();
1927
+ if (invoiceForm.elements.documentType) {
1928
+ invoiceForm.elements.documentType.value = "standard";
1929
+ }
1930
+ syncDocumentTypeControls();
1931
+ customExemptionNote = "";
1932
+ updateExemptionVisibility(false);
1933
  itemsBody.innerHTML = "";
1934
  createItemRow();
1935
  const now = new Date();
 
2176
  });
2177
  }
2178
 
2179
+ if (exemptionNoteInput) {
2180
+ exemptionNoteInput.addEventListener("input", () => {
2181
+ if (exemptionReasonSelect && exemptionReasonSelect.value === "custom") {
2182
+ customExemptionNote = exemptionNoteInput.value;
2183
+ }
2184
+ });
2185
+ }
2186
+
2187
+ if (documentTypeSelect) {
2188
+ documentTypeSelect.addEventListener("change", syncDocumentTypeControls);
2189
+ syncDocumentTypeControls();
2190
+ }
2191
+
2192
+ if (invoiceForm) {
2193
  invoiceForm.addEventListener("submit", async (event) => {
2194
  event.preventDefault();
2195
  try {
server.py CHANGED
@@ -521,14 +521,34 @@ def validate_client(payload: Dict[str, Any]) -> Dict[str, str]:
521
  "phone": normalize_phone(client_payload.get("phone") or payload.get("clientPhone")),
522
  }
523
  return client
524
-
525
-
 
 
 
 
 
 
 
526
  def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dict[str, str]) -> Dict[str, Any]:
527
  now = datetime.now()
528
- invoice_id = f"FV-{now.strftime('%Y%m%d-%H%M%S')}"
 
 
 
529
  issued_at = now.strftime("%Y-%m-%d %H:%M")
530
  sale_date = payload.get("sale_date") or payload.get("saleDate") or date.today().isoformat()
531
- payment_term = int(payload.get("payment_term") or payload.get("paymentTerm") or 14)
 
 
 
 
 
 
 
 
 
 
532
  items = payload.get("items") or []
533
 
534
  normalized_items: List[Dict[str, Any]] = []
@@ -599,10 +619,12 @@ def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dic
599
 
600
  exemption_note = (payload.get("exemption_note") or payload.get("exemptionNote") or "").strip()
601
 
602
- return {
603
- "invoice_id": invoice_id,
604
- "issued_at": issued_at,
605
- "sale_date": sale_date,
 
 
606
  "payment_term": payment_term,
607
  "items": normalized_items,
608
  "summary": summary_list,
@@ -710,6 +732,9 @@ def api_invoices() -> Any:
710
  for row in invoice_rows:
711
  issued_at_value = row.get("issued_at")
712
  sale_date_value = row.get("sale_date")
 
 
 
713
  if isinstance(issued_at_value, datetime):
714
  issued_at = issued_at_value.strftime("%Y-%m-%d %H:%M")
715
  else:
@@ -730,10 +755,12 @@ def api_invoices() -> Any:
730
  }
731
  invoices.append(
732
  {
733
- "invoice_id": row.get("invoice_number"),
 
 
734
  "issued_at": issued_at,
735
  "sale_date": sale_date,
736
- "payment_term": row.get("payment_term_days"),
737
  "exemption_note": row.get("exemption_note"),
738
  "items": items_map.get(row["id"], []),
739
  "summary": summary_map.get(row["id"], []),
 
521
  "phone": normalize_phone(client_payload.get("phone") or payload.get("clientPhone")),
522
  }
523
  return client
524
+
525
+
526
+ def normalize_document_type(payload: Dict[str, Any]) -> str:
527
+ document_type = (payload.get("document_type") or payload.get("documentType") or "standard").strip()
528
+ if document_type == "personal_paid":
529
+ return "personal_paid"
530
+ return "standard"
531
+
532
+
533
  def build_invoice(payload: Dict[str, Any], business: Dict[str, Any], client: Dict[str, str]) -> Dict[str, Any]:
534
  now = datetime.now()
535
+ document_type = normalize_document_type(payload)
536
+ payment_status = "paid" if document_type == "personal_paid" else "unpaid"
537
+ invoice_prefix = "FI" if document_type == "personal_paid" else "FV"
538
+ invoice_id = f"{invoice_prefix}-{now.strftime('%Y%m%d-%H%M%S')}"
539
  issued_at = now.strftime("%Y-%m-%d %H:%M")
540
  sale_date = payload.get("sale_date") or payload.get("saleDate") or date.today().isoformat()
541
+ payment_term_raw = payload.get("payment_term")
542
+ if payment_term_raw is None:
543
+ payment_term_raw = payload.get("paymentTerm")
544
+ if payment_term_raw in (None, ""):
545
+ payment_term = 0 if document_type == "personal_paid" else 14
546
+ else:
547
+ payment_term = int(payment_term_raw)
548
+ if document_type == "personal_paid":
549
+ payment_term = 0
550
+ if not client.get("name"):
551
+ raise ValueError("Podaj imie i nazwisko nabywcy dla faktury imiennej.")
552
  items = payload.get("items") or []
553
 
554
  normalized_items: List[Dict[str, Any]] = []
 
619
 
620
  exemption_note = (payload.get("exemption_note") or payload.get("exemptionNote") or "").strip()
621
 
622
+ return {
623
+ "invoice_id": invoice_id,
624
+ "document_type": document_type,
625
+ "payment_status": payment_status,
626
+ "issued_at": issued_at,
627
+ "sale_date": sale_date,
628
  "payment_term": payment_term,
629
  "items": normalized_items,
630
  "summary": summary_list,
 
732
  for row in invoice_rows:
733
  issued_at_value = row.get("issued_at")
734
  sale_date_value = row.get("sale_date")
735
+ invoice_number = row.get("invoice_number") or ""
736
+ payment_term_days = row.get("payment_term_days")
737
+ is_personal_paid = invoice_number.startswith("FI-") or payment_term_days == 0
738
  if isinstance(issued_at_value, datetime):
739
  issued_at = issued_at_value.strftime("%Y-%m-%d %H:%M")
740
  else:
 
755
  }
756
  invoices.append(
757
  {
758
+ "invoice_id": invoice_number,
759
+ "document_type": "personal_paid" if is_personal_paid else "standard",
760
+ "payment_status": "paid" if is_personal_paid else "unpaid",
761
  "issued_at": issued_at,
762
  "sale_date": sale_date,
763
+ "payment_term": payment_term_days,
764
  "exemption_note": row.get("exemption_note"),
765
  "items": items_map.get(row["id"], []),
766
  "summary": summary_map.get(row["id"], []),