Spaces:
Sleeping
Sleeping
Add paid personal invoice mode
Browse files
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 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1341 |
-
const
|
| 1342 |
-
const
|
|
|
|
| 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 |
-
|
| 1370 |
-
|
| 1371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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>
|
| 1420 |
-
<span><strong>
|
| 1421 |
-
<span><strong>Data
|
| 1422 |
-
|
| 1423 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 1857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 605 |
-
"
|
|
|
|
|
|
|
| 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":
|
|
|
|
|
|
|
| 734 |
"issued_at": issued_at,
|
| 735 |
"sale_date": sale_date,
|
| 736 |
-
"payment_term":
|
| 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"], []),
|