@fayz-ai/plugin-financial 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +523 -63
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/views/CashRegistersView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -8471,6 +8471,50 @@ function CashClosingPanel({
|
|
|
8471
8471
|
)
|
|
8472
8472
|
] });
|
|
8473
8473
|
}
|
|
8474
|
+
function dayKeyOf(iso) {
|
|
8475
|
+
if (!iso) return "";
|
|
8476
|
+
const d = new Date(iso);
|
|
8477
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
8478
|
+
}
|
|
8479
|
+
function groupByClosingDay(rows) {
|
|
8480
|
+
const days = [];
|
|
8481
|
+
const sorted = [...rows].sort((a, b) => (b.closedAt ?? "").localeCompare(a.closedAt ?? ""));
|
|
8482
|
+
for (const r of sorted) {
|
|
8483
|
+
const key = dayKeyOf(r.closedAt);
|
|
8484
|
+
const last = days[days.length - 1];
|
|
8485
|
+
if (last && last.key === key) last.rows.push(r);
|
|
8486
|
+
else days.push({ key, rows: [r] });
|
|
8487
|
+
}
|
|
8488
|
+
return days;
|
|
8489
|
+
}
|
|
8490
|
+
function DayHeader({ dayKey, count }) {
|
|
8491
|
+
const t2 = useTranslation();
|
|
8492
|
+
let label = "\u2014";
|
|
8493
|
+
if (dayKey === dayKeyOf((/* @__PURE__ */ new Date()).toISOString())) label = t2("financial.cash.reviewToday");
|
|
8494
|
+
else if (dayKey === dayKeyOf(new Date(Date.now() - 864e5).toISOString())) label = t2("financial.cash.reviewYesterday");
|
|
8495
|
+
else if (dayKey) {
|
|
8496
|
+
const [y, m, d] = dayKey.split("-").map(Number);
|
|
8497
|
+
label = new Date(y, m - 1, d).toLocaleDateString(void 0, { weekday: "long", day: "2-digit", month: "2-digit" });
|
|
8498
|
+
}
|
|
8499
|
+
return (
|
|
8500
|
+
// Stays on screen while its day scrolls, stopping under the page's tab band
|
|
8501
|
+
// like a list's column header. Opaque: rows pass underneath it.
|
|
8502
|
+
/* @__PURE__ */ jsxs(
|
|
8503
|
+
"div",
|
|
8504
|
+
{
|
|
8505
|
+
className: "sticky z-10 flex items-center gap-2 border-b bg-muted px-4 py-1.5 text-xs font-medium text-muted-foreground",
|
|
8506
|
+
style: { top: "var(--fz-page-band-h, 0px)" },
|
|
8507
|
+
children: [
|
|
8508
|
+
/* @__PURE__ */ jsx("span", { className: "first-letter:uppercase", children: label }),
|
|
8509
|
+
/* @__PURE__ */ jsx("span", { className: "rounded-full bg-background px-1.5 text-[10px] tabular-nums", children: count })
|
|
8510
|
+
]
|
|
8511
|
+
}
|
|
8512
|
+
)
|
|
8513
|
+
);
|
|
8514
|
+
}
|
|
8515
|
+
var HOUR_MINUTE = { hour: "2-digit", minute: "2-digit" };
|
|
8516
|
+
var PANEL_REVIEW_LIMIT = 3;
|
|
8517
|
+
var REVIEW_TAB_QUERY = "tab=review";
|
|
8474
8518
|
function ReviewQueue({
|
|
8475
8519
|
variant,
|
|
8476
8520
|
rows,
|
|
@@ -8484,6 +8528,11 @@ function ReviewQueue({
|
|
|
8484
8528
|
const me = user?.id ?? null;
|
|
8485
8529
|
const [reviewing, setReviewing] = useState(null);
|
|
8486
8530
|
const pendentes = (rows ?? []).filter((r) => r.reviewStatus === "pending");
|
|
8531
|
+
const setCashSessionModalOpen = useFinancialStore((s) => s.setCashSessionModalOpen);
|
|
8532
|
+
const compact = variant === "panel";
|
|
8533
|
+
const shown = compact ? [...pendentes].sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference) || (a.closedAt ?? "").localeCompare(b.closedAt ?? "")).slice(0, PANEL_REVIEW_LIMIT) : pendentes;
|
|
8534
|
+
const hidden = pendentes.length - shown.length;
|
|
8535
|
+
const days = compact ? [] : groupByClosingDay(pendentes);
|
|
8487
8536
|
const sectionClass = variant === "panel" ? "" : "rounded-card border bg-card shadow-sm";
|
|
8488
8537
|
return /* @__PURE__ */ jsxs("div", { className: sectionClass, children: [
|
|
8489
8538
|
showHeader && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-4 py-3", children: [
|
|
@@ -8491,39 +8540,68 @@ function ReviewQueue({
|
|
|
8491
8540
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.cash.reviewQueue") }),
|
|
8492
8541
|
pendentes.length > 0 && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-info/10 px-2 py-0.5 text-[10px] font-semibold text-info", children: pendentes.length })
|
|
8493
8542
|
] }),
|
|
8494
|
-
showHeader && /* @__PURE__ */ jsx("p", { className: "px-4 pt-3 text-[11px] text-muted-foreground", children: "Quem abre e fecha o caixa n\xE3o \xE9 quem confere. Um fechamento fica aqui at\xE9 algu\xE9m da ger\xEAncia ou do financeiro aprovar." }),
|
|
8543
|
+
showHeader && !compact && /* @__PURE__ */ jsx("p", { className: "px-4 pt-3 text-[11px] text-muted-foreground", children: "Quem abre e fecha o caixa n\xE3o \xE9 quem confere. Um fechamento fica aqui at\xE9 algu\xE9m da ger\xEAncia ou do financeiro aprovar." }),
|
|
8495
8544
|
error && /* @__PURE__ */ jsx("p", { className: "px-4 pt-2 text-xs text-destructive", children: error }),
|
|
8496
8545
|
rows === null ? /* @__PURE__ */ jsx("div", { className: "space-y-2 p-4", children: [0, 1].map((i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
8497
8546
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }),
|
|
8498
8547
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-32" })
|
|
8499
|
-
] }, i)) }) : pendentes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-6 text-center text-sm text-muted-foreground", children: t2("financial.cash.reviewQueueEmpty") }) : /* @__PURE__ */
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8548
|
+
] }, i)) }) : pendentes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-6 text-center text-sm text-muted-foreground", children: t2("financial.cash.reviewQueueEmpty") }) : /* @__PURE__ */ jsxs("div", { className: "divide-y", children: [
|
|
8549
|
+
(compact ? [{ key: "", rows: shown }] : days).map((day3) => /* @__PURE__ */ jsxs(React29__default.Fragment, { children: [
|
|
8550
|
+
!compact && /* @__PURE__ */ jsx(DayHeader, { dayKey: day3.key, count: day3.rows.length }),
|
|
8551
|
+
day3.rows.map((r) => {
|
|
8552
|
+
const fechei = !!me && r.closedByUserId === me;
|
|
8553
|
+
const amount = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8504
8554
|
formatCurrency(r.closingBalance, currency),
|
|
8505
8555
|
r.difference !== 0 && /* @__PURE__ */ jsxs("span", { className: `ml-2 text-xs tabular-nums ${r.difference > 0 ? "text-success" : "text-destructive"}`, children: [
|
|
8506
8556
|
r.difference > 0 ? "+" : "",
|
|
8507
8557
|
formatCurrency(r.difference, currency)
|
|
8508
8558
|
] })
|
|
8509
|
-
] })
|
|
8510
|
-
|
|
8511
|
-
|
|
8559
|
+
] });
|
|
8560
|
+
const action = fechei ? (
|
|
8561
|
+
// Estado que impede não oferece a ação — e diz por quê.
|
|
8562
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.cash.cannotReviewOwn") })
|
|
8563
|
+
) : /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "shrink-0", onClick: () => setReviewing(r.sessionId), children: [
|
|
8564
|
+
/* @__PURE__ */ jsx(ClipboardCheck, { className: "h-3.5 w-3.5" }),
|
|
8512
8565
|
" ",
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8566
|
+
t2("financial.cash.review")
|
|
8567
|
+
] });
|
|
8568
|
+
if (compact) {
|
|
8569
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-2", children: [
|
|
8570
|
+
/* @__PURE__ */ jsx("p", { className: "shrink-0 text-sm font-medium tabular-nums text-foreground", children: amount }),
|
|
8571
|
+
/* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: [
|
|
8572
|
+
r.closedByName ?? "\u2014",
|
|
8573
|
+
r.closedAt ? ` \xB7 ${new Date(r.closedAt).toLocaleDateString(void 0, { day: "2-digit", month: "2-digit" })}` : ""
|
|
8574
|
+
] }),
|
|
8575
|
+
action
|
|
8576
|
+
] }, r.sessionId);
|
|
8577
|
+
}
|
|
8578
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 px-4 py-3", children: [
|
|
8579
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
8580
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: amount }),
|
|
8581
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
8582
|
+
t2("financial.cash.closedBy"),
|
|
8583
|
+
" ",
|
|
8584
|
+
r.closedByName ?? "\u2014",
|
|
8585
|
+
r.closedAt ? ` \xB7 ${new Date(r.closedAt).toLocaleTimeString(void 0, HOUR_MINUTE)}` : ""
|
|
8586
|
+
] })
|
|
8587
|
+
] }),
|
|
8588
|
+
action
|
|
8589
|
+
] }, r.sessionId);
|
|
8590
|
+
})
|
|
8591
|
+
] }, day3.key || "all")),
|
|
8592
|
+
hidden > 0 && /* @__PURE__ */ jsxs(
|
|
8593
|
+
"a",
|
|
8594
|
+
{
|
|
8595
|
+
href: `#/financial/cash/registers?${REVIEW_TAB_QUERY}`,
|
|
8596
|
+
onClick: () => setCashSessionModalOpen(false),
|
|
8597
|
+
className: "flex items-center justify-between px-4 py-2.5 text-xs font-medium text-primary transition-colors hover:bg-muted/50",
|
|
8598
|
+
children: [
|
|
8599
|
+
t2("financial.cash.reviewSeeAll", { count: String(pendentes.length) }),
|
|
8600
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5" })
|
|
8601
|
+
]
|
|
8602
|
+
}
|
|
8603
|
+
)
|
|
8604
|
+
] }),
|
|
8527
8605
|
reviewing && /* @__PURE__ */ jsx(
|
|
8528
8606
|
CashSessionReviewModal,
|
|
8529
8607
|
{
|
|
@@ -8554,7 +8632,9 @@ function CashRegistersView({ variant = "page" } = {}) {
|
|
|
8554
8632
|
const setCashSessionModalOpen = useFinancialStore((s) => s.setCashSessionModalOpen);
|
|
8555
8633
|
const [expandedClosing, setExpandedClosing] = useState(null);
|
|
8556
8634
|
const [viewingDetails, setViewingDetails] = useState(null);
|
|
8557
|
-
const [tab, setTab] = useState(
|
|
8635
|
+
const [tab, setTab] = useState(
|
|
8636
|
+
() => typeof window !== "undefined" && window.location.hash.includes(REVIEW_TAB_QUERY) ? "review" : "open"
|
|
8637
|
+
);
|
|
8558
8638
|
const [reviewRows, setReviewRows] = useState(null);
|
|
8559
8639
|
const [reviewError, setReviewError] = useState(null);
|
|
8560
8640
|
const loadReviews = React29__default.useCallback(async () => {
|
|
@@ -8614,49 +8694,52 @@ function CashRegistersView({ variant = "page" } = {}) {
|
|
|
8614
8694
|
}
|
|
8615
8695
|
)
|
|
8616
8696
|
] }, session.id)) });
|
|
8617
|
-
const closedSessionsList = /* @__PURE__ */ jsx("div", { className: "divide-y", children: closedSessions.map((
|
|
8618
|
-
/* @__PURE__ */
|
|
8619
|
-
|
|
8620
|
-
/* @__PURE__ */ jsxs("
|
|
8621
|
-
|
|
8622
|
-
"
|
|
8623
|
-
|
|
8697
|
+
const closedSessionsList = /* @__PURE__ */ jsx("div", { className: "divide-y", children: groupByClosingDay(closedSessions).map((day3) => /* @__PURE__ */ jsxs(React29__default.Fragment, { children: [
|
|
8698
|
+
/* @__PURE__ */ jsx(DayHeader, { dayKey: day3.key, count: day3.rows.length }),
|
|
8699
|
+
day3.rows.map((session) => /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
8700
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
8701
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: session.bankAccountName ?? t2("financial.cash.title") }),
|
|
8702
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
8703
|
+
dayKeyOf(session.openedAt) === dayKeyOf(session.closedAt) ? new Date(session.openedAt).toLocaleTimeString(void 0, HOUR_MINUTE) : new Date(session.openedAt).toLocaleString(void 0, { day: "2-digit", month: "2-digit", ...HOUR_MINUTE }),
|
|
8704
|
+
" \u2014 ",
|
|
8705
|
+
session.closedAt ? new Date(session.closedAt).toLocaleTimeString(void 0, HOUR_MINUTE) : ""
|
|
8706
|
+
] }),
|
|
8707
|
+
session.openedByName && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
8708
|
+
t2("financial.cash.by"),
|
|
8709
|
+
" ",
|
|
8710
|
+
session.openedByName
|
|
8711
|
+
] })
|
|
8624
8712
|
] }),
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8713
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-start gap-3", children: [
|
|
8714
|
+
/* @__PURE__ */ jsxs("div", { className: "text-right", children: [
|
|
8715
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
|
|
8716
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
8717
|
+
t2("financial.cash.openLabel"),
|
|
8718
|
+
": ",
|
|
8719
|
+
formatCurrency(session.openingBalance, currency)
|
|
8720
|
+
] }),
|
|
8721
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
8722
|
+
t2("financial.cash.closeLabel"),
|
|
8723
|
+
": ",
|
|
8724
|
+
formatCurrency(session.closingBalance ?? 0, currency)
|
|
8725
|
+
] })
|
|
8638
8726
|
] }),
|
|
8639
|
-
/* @__PURE__ */ jsxs("
|
|
8640
|
-
t2("financial.cash.
|
|
8727
|
+
session.difference !== void 0 && session.difference !== 0 && /* @__PURE__ */ jsxs("p", { className: `text-xs font-semibold tabular-nums mt-0.5 ${session.difference > 0 ? "text-success" : "text-destructive"}`, children: [
|
|
8728
|
+
t2("financial.cash.diff"),
|
|
8641
8729
|
": ",
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
t2("financial.cash.diff"),
|
|
8647
|
-
": ",
|
|
8648
|
-
session.difference >= 0 ? "+" : "",
|
|
8649
|
-
formatCurrency(session.difference, currency)
|
|
8730
|
+
session.difference >= 0 ? "+" : "",
|
|
8731
|
+
formatCurrency(session.difference, currency)
|
|
8732
|
+
] }),
|
|
8733
|
+
session.notes && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5 max-w-[200px] truncate", title: session.notes, children: session.notes })
|
|
8650
8734
|
] }),
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
t2("financial.cash.details")
|
|
8735
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setViewingDetails(session.id), children: [
|
|
8736
|
+
/* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
|
|
8737
|
+
" ",
|
|
8738
|
+
t2("financial.cash.details")
|
|
8739
|
+
] })
|
|
8657
8740
|
] })
|
|
8658
|
-
] })
|
|
8659
|
-
] }
|
|
8741
|
+
] }) }, session.id))
|
|
8742
|
+
] }, day3.key || "all")) });
|
|
8660
8743
|
return /* @__PURE__ */ jsxs("div", { className: inPanel ? "" : "space-y-4", children: [
|
|
8661
8744
|
!inPanel && /* @__PURE__ */ jsx(
|
|
8662
8745
|
SubpageHeader,
|
|
@@ -11999,6 +12082,7 @@ function ReconciliationView() {
|
|
|
11999
12082
|
value: accountId,
|
|
12000
12083
|
displayValue: selectedAccount?.name ?? "",
|
|
12001
12084
|
icon: Landmark,
|
|
12085
|
+
leading: selectedAccount ? /* @__PURE__ */ jsx(AccountIcon, { account: selectedAccount }) : void 0,
|
|
12002
12086
|
placeholder: "Digite...",
|
|
12003
12087
|
noResultsLabel: t2("financial.reconciliation.noIntegratedAccount"),
|
|
12004
12088
|
searchingLabel: "Buscando contas...",
|
|
@@ -12016,6 +12100,7 @@ function ReconciliationView() {
|
|
|
12016
12100
|
id: account.id,
|
|
12017
12101
|
label: account.name,
|
|
12018
12102
|
subtitle: account.bankName || account.accountNumber,
|
|
12103
|
+
icon: /* @__PURE__ */ jsx(AccountIcon, { account }),
|
|
12019
12104
|
group: ACCOUNT_GROUPS2[account.accountType] ?? "Outras contas",
|
|
12020
12105
|
data: account
|
|
12021
12106
|
}));
|
|
@@ -20380,6 +20465,9 @@ var en = {
|
|
|
20380
20465
|
"financial.cash.by": "by",
|
|
20381
20466
|
"financial.cash.closeThisSession": "Close this session",
|
|
20382
20467
|
"financial.cash.reviewQueue": "Close review",
|
|
20468
|
+
"financial.cash.reviewSeeAll": "See all {count} closes",
|
|
20469
|
+
"financial.cash.reviewToday": "Today",
|
|
20470
|
+
"financial.cash.reviewYesterday": "Yesterday",
|
|
20383
20471
|
"financial.cash.reviewQueueEmpty": "No closes waiting for review.",
|
|
20384
20472
|
"financial.cash.closedBy": "Closed by",
|
|
20385
20473
|
"financial.cash.approve": "Approve",
|
|
@@ -21336,6 +21424,9 @@ var ptBR = {
|
|
|
21336
21424
|
"financial.cash.by": "por",
|
|
21337
21425
|
"financial.cash.closeThisSession": "Fechar esta sess\xE3o",
|
|
21338
21426
|
"financial.cash.reviewQueue": "Confer\xEAncia de fechamentos",
|
|
21427
|
+
"financial.cash.reviewSeeAll": "Ver todos os {count} fechamentos",
|
|
21428
|
+
"financial.cash.reviewToday": "Hoje",
|
|
21429
|
+
"financial.cash.reviewYesterday": "Ontem",
|
|
21339
21430
|
"financial.cash.reviewQueueEmpty": "Nenhum fechamento esperando confer\xEAncia.",
|
|
21340
21431
|
"financial.cash.closedBy": "Fechado por",
|
|
21341
21432
|
"financial.cash.approve": "Aprovar",
|
|
@@ -48480,6 +48571,374 @@ COMMENT ON COLUMN public.plg_financial_invoices.tags IS 'Free labels the house p
|
|
|
48480
48571
|
CREATE INDEX IF NOT EXISTS plg_financial_invoices_tags_idx
|
|
48481
48572
|
ON public.plg_financial_invoices USING gin (tags);
|
|
48482
48573
|
`;
|
|
48574
|
+
var MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE = `-- 131 \u2014 The bill follows the price of the sale.
|
|
48575
|
+
--
|
|
48576
|
+
-- A receivable is raised when the sale closes, at the total of that instant.
|
|
48577
|
+
-- Both bridges (\`order_raise_receivable\`, \`fn_invoice_from_order_internal\`) are
|
|
48578
|
+
-- idempotent: once the invoice exists they return without looking at the
|
|
48579
|
+
-- amount. So a price changed AFTER closing never reached the bill \u2014 a clinic
|
|
48580
|
+
-- had an appointment at R$ 700 whose receivable still asked for R$ 140, and
|
|
48581
|
+
-- the only symptom was two different numbers on two screens.
|
|
48582
|
+
--
|
|
48583
|
+
-- The rule, in one place:
|
|
48584
|
+
-- * nothing received yet \u2192 the invoice, its lines and its open instalments
|
|
48585
|
+
-- take the sale's total;
|
|
48586
|
+
-- * something received \u2192 nothing moves. A paid invoice is immutable
|
|
48587
|
+
-- (PRD 04, 120); the screens show both numbers and a person decides;
|
|
48588
|
+
-- * the sale drops to zero \u2192 the unpaid invoice is cancelled, not left at 0.
|
|
48589
|
+
--
|
|
48590
|
+
-- The lines follow the model the bill already has. A bill raised here owns a
|
|
48591
|
+
-- copy of the sale's lines (010) and that copy is refreshed. A bill ported
|
|
48592
|
+
-- from V1 has no lines of its own \u2014 \`v_financial_invoice_lines\` reads them
|
|
48593
|
+
-- from the order \u2014 and it stays that way: giving it a copy would freeze lines
|
|
48594
|
+
-- the sync still writes on the order.
|
|
48595
|
+
--
|
|
48596
|
+
-- Receivables of SELLING orders only. A purchase's bill diverges from its
|
|
48597
|
+
-- order on purpose (interest, supplier credit \u2014 120).
|
|
48598
|
+
|
|
48599
|
+
-- \u2500\u2500 1. the reconciliation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
48600
|
+
CREATE OR REPLACE FUNCTION public.order_reconcile_receivable_internal(p_order_id uuid)
|
|
48601
|
+
RETURNS text
|
|
48602
|
+
LANGUAGE plpgsql
|
|
48603
|
+
SECURITY DEFINER
|
|
48604
|
+
SET search_path TO ''
|
|
48605
|
+
AS $$
|
|
48606
|
+
DECLARE
|
|
48607
|
+
v_order public.orders%ROWTYPE;
|
|
48608
|
+
v_invoice public.plg_financial_invoices%ROWTYPE;
|
|
48609
|
+
v_total numeric;
|
|
48610
|
+
v_paid numeric;
|
|
48611
|
+
v_open_sum numeric;
|
|
48612
|
+
v_last uuid;
|
|
48613
|
+
v_summary text;
|
|
48614
|
+
v_own boolean;
|
|
48615
|
+
BEGIN
|
|
48616
|
+
SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
|
|
48617
|
+
IF NOT FOUND OR NOT (v_order.kind = ANY (public.order_selling_kinds())) THEN
|
|
48618
|
+
RETURN 'none';
|
|
48619
|
+
END IF;
|
|
48620
|
+
|
|
48621
|
+
-- (tenant_id, order_id) is the index; naming the tenant is what uses it.
|
|
48622
|
+
SELECT * INTO v_invoice FROM public.plg_financial_invoices
|
|
48623
|
+
WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id
|
|
48624
|
+
AND cancelled_at IS NULL AND direction = 'receivable'
|
|
48625
|
+
ORDER BY created_at LIMIT 1;
|
|
48626
|
+
IF NOT FOUND THEN RETURN 'none'; END IF;
|
|
48627
|
+
|
|
48628
|
+
-- A sale billed across several live invoices has no single bill that should
|
|
48629
|
+
-- carry its whole total. Nobody does this today; deciding it here would be a guess.
|
|
48630
|
+
IF (SELECT count(*) FROM public.plg_financial_invoices
|
|
48631
|
+
WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id
|
|
48632
|
+
AND cancelled_at IS NULL AND direction = 'receivable') > 1 THEN
|
|
48633
|
+
RETURN 'none';
|
|
48634
|
+
END IF;
|
|
48635
|
+
|
|
48636
|
+
v_total := round(coalesce(v_order.total, 0), 2);
|
|
48637
|
+
|
|
48638
|
+
SELECT coalesce(sum(b.paid), 0) INTO v_paid
|
|
48639
|
+
FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice.id;
|
|
48640
|
+
|
|
48641
|
+
v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_invoice.id);
|
|
48642
|
+
|
|
48643
|
+
IF round(coalesce(v_invoice.total, 0), 2) = v_total THEN
|
|
48644
|
+
-- Same amount, but the lines may still be the old ones: a screen that
|
|
48645
|
+
-- writes the total and THEN the lines reaches here with the bill already
|
|
48646
|
+
-- at the new amount. A line with no live sale line behind it is stale.
|
|
48647
|
+
IF v_paid = 0 AND v_own AND (
|
|
48648
|
+
(SELECT count(*) FROM public.items WHERE invoice_id = v_invoice.id)
|
|
48649
|
+
IS DISTINCT FROM (SELECT count(*) FROM public.items WHERE order_id = p_order_id)
|
|
48650
|
+
OR EXISTS (
|
|
48651
|
+
SELECT 1 FROM public.items l
|
|
48652
|
+
WHERE l.invoice_id = v_invoice.id
|
|
48653
|
+
AND NOT EXISTS (
|
|
48654
|
+
SELECT 1 FROM public.item_associations a
|
|
48655
|
+
JOIN public.items s ON s.id = a.from_item_id
|
|
48656
|
+
WHERE a.to_item_id = l.id AND s.order_id = p_order_id AND s.total = l.total))
|
|
48657
|
+
) THEN
|
|
48658
|
+
DELETE FROM public.items WHERE invoice_id = v_invoice.id;
|
|
48659
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);
|
|
48660
|
+
END IF;
|
|
48661
|
+
RETURN 'in_sync';
|
|
48662
|
+
END IF;
|
|
48663
|
+
|
|
48664
|
+
IF v_paid > 0 THEN RETURN 'kept_paid'; END IF;
|
|
48665
|
+
|
|
48666
|
+
IF v_total <= 0 THEN
|
|
48667
|
+
UPDATE public.plg_financial_invoices
|
|
48668
|
+
SET cancelled_at = now(),
|
|
48669
|
+
cancel_reason = coalesce(cancel_reason, 'The sale it was raised from no longer has an amount')
|
|
48670
|
+
WHERE id = v_invoice.id;
|
|
48671
|
+
RETURN 'amended';
|
|
48672
|
+
END IF;
|
|
48673
|
+
|
|
48674
|
+
SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
|
|
48675
|
+
FROM public.items WHERE order_id = p_order_id;
|
|
48676
|
+
|
|
48677
|
+
UPDATE public.plg_financial_invoices
|
|
48678
|
+
SET total = v_total,
|
|
48679
|
+
description = coalesce(nullif(v_summary, ''), description)
|
|
48680
|
+
WHERE id = v_invoice.id;
|
|
48681
|
+
|
|
48682
|
+
-- A stale copy is replaced whole. The copier refuses to run over existing
|
|
48683
|
+
-- lines, hence the delete first.
|
|
48684
|
+
IF v_own THEN
|
|
48685
|
+
DELETE FROM public.items WHERE invoice_id = v_invoice.id;
|
|
48686
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);
|
|
48687
|
+
END IF;
|
|
48688
|
+
|
|
48689
|
+
-- The open instalments keep their proportions; the last one takes the cents.
|
|
48690
|
+
SELECT coalesce(sum(amount), 0) INTO v_open_sum
|
|
48691
|
+
FROM public.plg_financial_invoice_installments
|
|
48692
|
+
WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL;
|
|
48693
|
+
SELECT id INTO v_last FROM public.plg_financial_invoice_installments
|
|
48694
|
+
WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL
|
|
48695
|
+
ORDER BY number DESC LIMIT 1;
|
|
48696
|
+
|
|
48697
|
+
IF v_last IS NULL THEN
|
|
48698
|
+
INSERT INTO public.plg_financial_invoice_installments
|
|
48699
|
+
(tenant_id, unit_id, invoice_id, number, due_date, amount)
|
|
48700
|
+
VALUES (v_invoice.tenant_id, v_invoice.unit_id, v_invoice.id,
|
|
48701
|
+
coalesce((SELECT max(number) FROM public.plg_financial_invoice_installments
|
|
48702
|
+
WHERE invoice_id = v_invoice.id), 0) + 1,
|
|
48703
|
+
current_date, v_total);
|
|
48704
|
+
ELSE
|
|
48705
|
+
IF v_open_sum > 0 THEN
|
|
48706
|
+
UPDATE public.plg_financial_invoice_installments
|
|
48707
|
+
SET amount = round(v_total * amount / v_open_sum, 2)
|
|
48708
|
+
WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last;
|
|
48709
|
+
END IF;
|
|
48710
|
+
UPDATE public.plg_financial_invoice_installments
|
|
48711
|
+
SET amount = v_total - coalesce((
|
|
48712
|
+
SELECT sum(amount) FROM public.plg_financial_invoice_installments
|
|
48713
|
+
WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last), 0)
|
|
48714
|
+
WHERE id = v_last;
|
|
48715
|
+
END IF;
|
|
48716
|
+
|
|
48717
|
+
RETURN 'amended';
|
|
48718
|
+
EXCEPTION WHEN OTHERS THEN
|
|
48719
|
+
-- A closed period, or any other refusal from the books, never fails the
|
|
48720
|
+
-- sale's own write (same rule as raising the receivable, 001). The block
|
|
48721
|
+
-- rolls back whatever it had started, so the bill is left whole.
|
|
48722
|
+
RAISE WARNING 'order_reconcile_receivable: sale % kept its bill as it was: %', p_order_id, SQLERRM;
|
|
48723
|
+
RETURN 'kept_closed';
|
|
48724
|
+
END $$;
|
|
48725
|
+
|
|
48726
|
+
COMMENT ON FUNCTION public.order_reconcile_receivable_internal(uuid) IS
|
|
48727
|
+
'Makes an UNPAID receivable take its sale''s current total, lines and instalments. Returns none | in_sync | amended | kept_paid | kept_closed. A paid invoice is never touched (131).';
|
|
48728
|
+
|
|
48729
|
+
REVOKE ALL ON FUNCTION public.order_reconcile_receivable_internal(uuid) FROM PUBLIC, anon, authenticated;
|
|
48730
|
+
GRANT EXECUTE ON FUNCTION public.order_reconcile_receivable_internal(uuid) TO service_role;
|
|
48731
|
+
|
|
48732
|
+
-- \u2500\u2500 2. the door a screen uses after rewriting the sale's lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
48733
|
+
-- The trigger below fires on the total; a screen that writes the total and
|
|
48734
|
+
-- THEN the lines (the agenda does) calls this once it is done, so the bill's
|
|
48735
|
+
-- lines are the final ones.
|
|
48736
|
+
CREATE OR REPLACE FUNCTION public.order_reconcile_receivable(p_order_id uuid)
|
|
48737
|
+
RETURNS text
|
|
48738
|
+
LANGUAGE plpgsql
|
|
48739
|
+
SECURITY DEFINER
|
|
48740
|
+
SET search_path TO ''
|
|
48741
|
+
AS $$
|
|
48742
|
+
DECLARE v_tenant uuid;
|
|
48743
|
+
BEGIN
|
|
48744
|
+
SELECT tenant_id INTO v_tenant FROM public.orders WHERE id = p_order_id;
|
|
48745
|
+
IF v_tenant IS NULL OR v_tenant IS DISTINCT FROM app.current_tenant_id() THEN
|
|
48746
|
+
RAISE EXCEPTION 'order_reconcile_receivable: forbidden' USING ERRCODE = '42501';
|
|
48747
|
+
END IF;
|
|
48748
|
+
RETURN public.order_reconcile_receivable_internal(p_order_id);
|
|
48749
|
+
END $$;
|
|
48750
|
+
|
|
48751
|
+
COMMENT ON FUNCTION public.order_reconcile_receivable(uuid) IS
|
|
48752
|
+
'Tenant-checked door to order_reconcile_receivable_internal: it only ever aligns a bill with a sale the caller''s tenant already owns (131).';
|
|
48753
|
+
|
|
48754
|
+
REVOKE ALL ON FUNCTION public.order_reconcile_receivable(uuid) FROM PUBLIC, anon;
|
|
48755
|
+
GRANT EXECUTE ON FUNCTION public.order_reconcile_receivable(uuid) TO authenticated, service_role;
|
|
48756
|
+
|
|
48757
|
+
-- \u2500\u2500 3. the guarantee: any writer of the total, known or future \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
48758
|
+
CREATE OR REPLACE FUNCTION public.trg_order_total_reaches_its_invoice()
|
|
48759
|
+
RETURNS trigger
|
|
48760
|
+
LANGUAGE plpgsql
|
|
48761
|
+
SECURITY DEFINER
|
|
48762
|
+
SET search_path TO ''
|
|
48763
|
+
AS $$
|
|
48764
|
+
BEGIN
|
|
48765
|
+
PERFORM public.order_reconcile_receivable_internal(NEW.id);
|
|
48766
|
+
RETURN NULL;
|
|
48767
|
+
END $$;
|
|
48768
|
+
|
|
48769
|
+
DROP TRIGGER IF EXISTS orders_total_reaches_the_invoice ON public.orders;
|
|
48770
|
+
CREATE TRIGGER orders_total_reaches_the_invoice
|
|
48771
|
+
AFTER UPDATE OF total ON public.orders
|
|
48772
|
+
FOR EACH ROW
|
|
48773
|
+
WHEN (NEW.total IS DISTINCT FROM OLD.total)
|
|
48774
|
+
EXECUTE FUNCTION public.trg_order_total_reaches_its_invoice();
|
|
48775
|
+
|
|
48776
|
+
-- \u2500\u2500 4. raising again is the natural repair, and now it repairs the amount \u2500\u2500\u2500
|
|
48777
|
+
CREATE OR REPLACE FUNCTION public.order_raise_receivable(p_order_id uuid, p_due_date date DEFAULT NULL::date)
|
|
48778
|
+
RETURNS uuid
|
|
48779
|
+
LANGUAGE plpgsql
|
|
48780
|
+
SECURITY DEFINER
|
|
48781
|
+
SET search_path TO ''
|
|
48782
|
+
AS $$
|
|
48783
|
+
DECLARE
|
|
48784
|
+
v_order public.orders%ROWTYPE;
|
|
48785
|
+
v_invoice_id uuid;
|
|
48786
|
+
v_number text;
|
|
48787
|
+
v_summary text;
|
|
48788
|
+
v_total numeric;
|
|
48789
|
+
BEGIN
|
|
48790
|
+
SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
|
|
48791
|
+
IF NOT FOUND THEN RETURN NULL; END IF;
|
|
48792
|
+
|
|
48793
|
+
SELECT id INTO v_invoice_id FROM public.plg_financial_invoices
|
|
48794
|
+
WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;
|
|
48795
|
+
IF v_invoice_id IS NOT NULL THEN
|
|
48796
|
+
-- Re-raising stamps the lines on a bill that has none, as before (010),
|
|
48797
|
+
-- and now also repairs the amount.
|
|
48798
|
+
PERFORM public.order_reconcile_receivable_internal(p_order_id);
|
|
48799
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
|
|
48800
|
+
RETURN v_invoice_id;
|
|
48801
|
+
END IF;
|
|
48802
|
+
|
|
48803
|
+
v_total := round(coalesce(v_order.total, 0), 2);
|
|
48804
|
+
IF v_total <= 0 THEN RETURN NULL; END IF;
|
|
48805
|
+
|
|
48806
|
+
SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
|
|
48807
|
+
FROM public.items WHERE order_id = p_order_id;
|
|
48808
|
+
|
|
48809
|
+
v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, 'receivable');
|
|
48810
|
+
|
|
48811
|
+
INSERT INTO public.plg_financial_invoices
|
|
48812
|
+
(tenant_id, unit_id, direction, counterparty_person_id, order_id, number,
|
|
48813
|
+
competence_date, issue_date, total, description, metadata, created_by)
|
|
48814
|
+
VALUES
|
|
48815
|
+
(v_order.tenant_id, v_order.unit_id, 'receivable', v_order.party_id, p_order_id, v_number,
|
|
48816
|
+
coalesce(v_order.created_at::date, current_date), current_date,
|
|
48817
|
+
v_total, nullif(v_summary, ''),
|
|
48818
|
+
jsonb_build_object('source', 'order_raise_receivable', 'orderKind', v_order.kind),
|
|
48819
|
+
auth.uid())
|
|
48820
|
+
RETURNING id INTO v_invoice_id;
|
|
48821
|
+
|
|
48822
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
|
|
48823
|
+
|
|
48824
|
+
INSERT INTO public.plg_financial_invoice_installments
|
|
48825
|
+
(tenant_id, unit_id, invoice_id, number, due_date, amount)
|
|
48826
|
+
VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, 1,
|
|
48827
|
+
coalesce(p_due_date, current_date), v_total);
|
|
48828
|
+
|
|
48829
|
+
UPDATE public.orders SET
|
|
48830
|
+
stage = 'invoiced',
|
|
48831
|
+
direction = 'credit',
|
|
48832
|
+
metadata = coalesce(metadata, '{}'::jsonb)
|
|
48833
|
+
|| jsonb_build_object('invoiceNumber', v_number, 'installmentCount', 1),
|
|
48834
|
+
updated_at = now()
|
|
48835
|
+
WHERE id = p_order_id;
|
|
48836
|
+
|
|
48837
|
+
RETURN v_invoice_id;
|
|
48838
|
+
END $$;
|
|
48839
|
+
|
|
48840
|
+
CREATE OR REPLACE FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date DEFAULT NULL::date, p_status text DEFAULT NULL::text, p_installments integer DEFAULT 1, p_direction text DEFAULT 'credit'::text)
|
|
48841
|
+
RETURNS uuid
|
|
48842
|
+
LANGUAGE plpgsql
|
|
48843
|
+
SECURITY DEFINER
|
|
48844
|
+
SET search_path TO ''
|
|
48845
|
+
AS $$
|
|
48846
|
+
DECLARE
|
|
48847
|
+
v_order public.orders%ROWTYPE;
|
|
48848
|
+
v_invoice_id uuid;
|
|
48849
|
+
v_dir text;
|
|
48850
|
+
v_number text;
|
|
48851
|
+
v_summary text;
|
|
48852
|
+
v_contact text;
|
|
48853
|
+
v_n int := GREATEST(COALESCE(p_installments, 1), 1);
|
|
48854
|
+
v_total numeric;
|
|
48855
|
+
v_base numeric;
|
|
48856
|
+
v_amt numeric;
|
|
48857
|
+
v_alloc numeric := 0;
|
|
48858
|
+
v_due date;
|
|
48859
|
+
v_status text;
|
|
48860
|
+
i int;
|
|
48861
|
+
BEGIN
|
|
48862
|
+
SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
|
|
48863
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;
|
|
48864
|
+
|
|
48865
|
+
SELECT id INTO v_invoice_id FROM public.plg_financial_invoices
|
|
48866
|
+
WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;
|
|
48867
|
+
IF v_invoice_id IS NOT NULL THEN
|
|
48868
|
+
-- Idempotent, and the natural repair: an order invoiced before 010 gets its
|
|
48869
|
+
-- lines, and one whose price moved since gets its amount (131). The
|
|
48870
|
+
-- reconciliation is a no-op on anything but an unpaid receivable.
|
|
48871
|
+
PERFORM public.order_reconcile_receivable_internal(p_order_id);
|
|
48872
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
|
|
48873
|
+
RETURN p_order_id;
|
|
48874
|
+
END IF;
|
|
48875
|
+
IF EXISTS (SELECT 1 FROM public.plg_financial_movements
|
|
48876
|
+
WHERE invoice_id = p_order_id AND movement_kind = 'bill'
|
|
48877
|
+
AND status <> 'cancelled') THEN
|
|
48878
|
+
RETURN p_order_id;
|
|
48879
|
+
END IF;
|
|
48880
|
+
|
|
48881
|
+
v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;
|
|
48882
|
+
v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);
|
|
48883
|
+
|
|
48884
|
+
SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
|
|
48885
|
+
FROM public.items WHERE order_id = p_order_id;
|
|
48886
|
+
SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;
|
|
48887
|
+
|
|
48888
|
+
v_status := v_order.status;
|
|
48889
|
+
IF p_status IS NOT NULL AND EXISTS (
|
|
48890
|
+
SELECT 1 FROM public.order_kinds k
|
|
48891
|
+
WHERE k.kind = v_order.kind
|
|
48892
|
+
AND (k.statuses IS NULL OR p_status = ANY (k.statuses))
|
|
48893
|
+
) THEN
|
|
48894
|
+
v_status := p_status;
|
|
48895
|
+
END IF;
|
|
48896
|
+
|
|
48897
|
+
UPDATE public.orders SET
|
|
48898
|
+
status = v_status,
|
|
48899
|
+
stage = 'invoiced',
|
|
48900
|
+
direction = p_direction,
|
|
48901
|
+
metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
|
|
48902
|
+
'itemsSummary', COALESCE(v_summary, ''),
|
|
48903
|
+
'contactName', v_contact,
|
|
48904
|
+
'installmentCount', v_n,
|
|
48905
|
+
'quoteNumber', v_order.reference_number,
|
|
48906
|
+
'direction', p_direction,
|
|
48907
|
+
'invoiceNumber', v_number
|
|
48908
|
+
),
|
|
48909
|
+
updated_at = now()
|
|
48910
|
+
WHERE id = p_order_id;
|
|
48911
|
+
|
|
48912
|
+
v_total := round(COALESCE(v_order.total, 0), 2);
|
|
48913
|
+
v_due := COALESCE(p_due_date, current_date);
|
|
48914
|
+
|
|
48915
|
+
INSERT INTO public.plg_financial_invoices
|
|
48916
|
+
(tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,
|
|
48917
|
+
total, description, metadata, created_by)
|
|
48918
|
+
VALUES
|
|
48919
|
+
(v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,
|
|
48920
|
+
COALESCE(v_order.created_at::date, current_date), current_date,
|
|
48921
|
+
v_total, NULLIF(v_summary, ''),
|
|
48922
|
+
jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),
|
|
48923
|
+
auth.uid())
|
|
48924
|
+
RETURNING id INTO v_invoice_id;
|
|
48925
|
+
|
|
48926
|
+
PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
|
|
48927
|
+
|
|
48928
|
+
v_base := round(v_total / v_n, 2);
|
|
48929
|
+
FOR i IN 1..v_n LOOP
|
|
48930
|
+
IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);
|
|
48931
|
+
ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;
|
|
48932
|
+
|
|
48933
|
+
INSERT INTO public.plg_financial_invoice_installments
|
|
48934
|
+
(tenant_id, unit_id, invoice_id, number, due_date, amount)
|
|
48935
|
+
VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, i,
|
|
48936
|
+
(v_due + ((i - 1) || ' months')::interval)::date, v_amt);
|
|
48937
|
+
END LOOP;
|
|
48938
|
+
|
|
48939
|
+
RETURN p_order_id;
|
|
48940
|
+
END $$;
|
|
48941
|
+
`;
|
|
48483
48942
|
var MIGRATIONS = [
|
|
48484
48943
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
48485
48944
|
{ id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
|
|
@@ -48611,7 +49070,8 @@ var MIGRATIONS = [
|
|
|
48611
49070
|
{ id: "127_a_bill_line_enters_stock_as_it_really_arrived", sql: MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED },
|
|
48612
49071
|
{ id: "128_a_stock_receipt_shows_what_was_paid", sql: MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID },
|
|
48613
49072
|
{ id: "129_a_bill_can_be_written_in_another_currency", sql: MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY },
|
|
48614
|
-
{ id: "130_a_bill_can_be_tagged", sql: MIGRATION_130_A_BILL_CAN_BE_TAGGED }
|
|
49073
|
+
{ id: "130_a_bill_can_be_tagged", sql: MIGRATION_130_A_BILL_CAN_BE_TAGGED },
|
|
49074
|
+
{ id: "131_the_bill_follows_the_price_of_the_sale", sql: MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE }
|
|
48615
49075
|
];
|
|
48616
49076
|
var STANDALONE_CONFIG = resolveConfig();
|
|
48617
49077
|
registerTranslations(financialLocales);
|