@fayz-ai/plugin-financial 0.22.2 → 0.23.0
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/components/CancelInvoiceDialog.d.ts +15 -0
- package/dist/components/CancelInvoiceDialog.d.ts.map +1 -1
- package/dist/components/CashSessionHeaderControl.d.ts +2 -1
- package/dist/components/CashSessionHeaderControl.d.ts.map +1 -1
- package/dist/components/FinancialGeneralSettings.d.ts +2 -0
- package/dist/components/FinancialGeneralSettings.d.ts.map +1 -1
- package/dist/components/InstallmentRow.d.ts +4 -1
- package/dist/components/InstallmentRow.d.ts.map +1 -1
- package/dist/components/PaymentReceiptModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts.map +1 -1
- package/dist/components/ReceiveStockDialog.d.ts +22 -0
- package/dist/components/ReceiveStockDialog.d.ts.map +1 -0
- package/dist/components/StockEntryNote.d.ts +29 -0
- package/dist/components/StockEntryNote.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +56 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +2691 -698
- package/dist/index.js.map +1 -1
- package/dist/lib/fx-auto.d.ts +7 -0
- package/dist/lib/fx-auto.d.ts.map +1 -0
- package/dist/lib/installment-amounts.d.ts +18 -0
- package/dist/lib/installment-amounts.d.ts.map +1 -0
- package/dist/lib/item-icons.d.ts +6 -0
- package/dist/lib/item-icons.d.ts.map +1 -0
- 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 +9 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/FinancialPage.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
3
|
-
import { recordRailFields, RECORD_GROUP, activeFacet, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, getSupabaseClientOptional, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, useUnitMode, useDataChanged, countByTenant, tl, resolveDataProvider, useActiveUnitId, getActiveTenantId, emitDataChanged, describeError, exportCSV } from '@fayz-ai/core';
|
|
4
|
-
import { INLINE_LINK_TONE, SearchSelect, ModalTask, toast, Button, useInModalTaskHost, useSaveBar, tourAnchor, DatePicker, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget,
|
|
1
|
+
import * as React29 from 'react';
|
|
2
|
+
import React29__default, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
3
|
+
import { recordRailFields, RECORD_GROUP, activeFacet, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, getSupabaseClientOptional, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, useUnitMode, useDataChanged, countByTenant, tl, resolveDataProvider, useActiveUnitId, resolveCurrency, getActiveTenantId, emitDataChanged, describeError, exportCSV } from '@fayz-ai/core';
|
|
4
|
+
import { INLINE_LINK_TONE, SearchSelect, ModalTask, toast, Button, useInModalTaskHost, useSaveBar, tourAnchor, DatePicker, SegmentedControl, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, DataTable, Card, CardContent, Skeleton, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MultiSelect, PageTransition, ModulePage, Badge, TooltipProvider, ConfirmDialog, CurrencyInput, Input, PageHeaderActions, ChoiceSelect, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, Checkbox, Hint, ModalDescription, Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverTrigger, PopoverContent, PageTabs, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, RequiredFilter, FilterMenuRow, openPrintWindow, printDocument, ChoiceList, ModalFlow, Switch, WizardSteps, KpiCard, ChartWidget, CardHeader, CardTitle, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
|
|
5
5
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, createCrudPage, PluginSettingsPanel, dedup, useUnitFilter, useLimitGuard, usePermissionOptional, useLookupCreate, OrderFormSkeleton, WidgetSlot, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, adminNavigateTo, OrderFormAddItemStep, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, useModuleNavAccess, PluginOnboardingGateSkeleton, ModuleActionBar, SettingsGroup, ToggleRow, createArchetypeLookup, invalidateLimit, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, useChatStore, revealPage, useAuth, GetStarted,
|
|
7
|
-
import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban,
|
|
6
|
+
import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, createCrudPage, PluginSettingsPanel, dedup, useCurrency, useUnitFilter, useLimitGuard, usePermissionOptional, useLookupCreate, OrderFormSkeleton, WidgetSlot, OrderFormBlock, TagsField, OrderFormView, OrderFormItemsCard, OrderFormItemRow, adminNavigateTo, OrderFormAddItemStep, CurrencyScope, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, TagChip, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, useModuleNavAccess, PluginOnboardingGateSkeleton, ModuleActionBar, SettingsGroup, ToggleRow, createArchetypeLookup, invalidateLimit, useAgentSurface, useHouseCurrency, parseViewId, EntitlementGate, UpgradePrompt, useChatStore, revealPage, resolveEntityRoute, useAuth, GetStarted, PermissionGate, createOneShotAsk, askAI, requestChatUpload, toInlineFile, shrinkForVision } from '@fayz-ai/admin';
|
|
7
|
+
import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, Receipt, Gift, Briefcase, Package, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, PackagePlus, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Paperclip, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, Image, UploadCloud, Calendar, User, Unlink, BookOpen, Info, FolderOpen, Folder, RotateCcw, ChevronDown, Settings, ArrowLeft, ChevronLeft, Eye, Inbox, Clock, ClipboardCheck, Calculator, Square, Play, ChevronUp, Printer, CalendarClock, BadgeCheck, Target, ArrowRight, Users, RefreshCw, Download, Camera, Sparkles, Link2, FileSearch, CheckCheck, Pause, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, FileDown, ArrowUpRight, ArrowDownRight, EyeOff } from 'lucide-react';
|
|
8
8
|
import { createPortal } from 'react-dom';
|
|
9
9
|
import { createStore as createStore$1 } from 'zustand/vanilla';
|
|
10
10
|
import { useStore } from 'zustand';
|
|
@@ -550,7 +550,7 @@ function parseAmountInWords(raw) {
|
|
|
550
550
|
if (!words.length) return null;
|
|
551
551
|
const centsAt = words.findIndex((w) => w === "centavo" || w === "centavos");
|
|
552
552
|
const whole = centsAt >= 0 ? words.slice(0, centsAt) : words;
|
|
553
|
-
const
|
|
553
|
+
const cents2 = centsAt >= 0 ? sumGroup(words.slice(0, centsAt).slice(lastCurrencyIndex(words, centsAt))) : 0;
|
|
554
554
|
let total = 0;
|
|
555
555
|
let group = [];
|
|
556
556
|
let sawNumber = false;
|
|
@@ -570,7 +570,7 @@ function parseAmountInWords(raw) {
|
|
|
570
570
|
}
|
|
571
571
|
total += sumGroup(group);
|
|
572
572
|
if (!sawNumber) return null;
|
|
573
|
-
const value = centsAt >= 0 &&
|
|
573
|
+
const value = centsAt >= 0 && cents2 > 0 && cents2 < 100 ? total - cents2 + cents2 / 100 : total;
|
|
574
574
|
return value > 0 ? Math.round(value * 100) / 100 : null;
|
|
575
575
|
}
|
|
576
576
|
function lastCurrencyIndex(words, before) {
|
|
@@ -908,9 +908,9 @@ function startReceiptCapture(labels) {
|
|
|
908
908
|
});
|
|
909
909
|
}
|
|
910
910
|
function ReceiptAgentSurface({ onOpenDraft }) {
|
|
911
|
-
const live =
|
|
911
|
+
const live = React29.useRef(onOpenDraft);
|
|
912
912
|
live.current = onOpenDraft;
|
|
913
|
-
const contribution =
|
|
913
|
+
const contribution = React29.useMemo(() => ({
|
|
914
914
|
id: "financial.payables.receipt",
|
|
915
915
|
pluginId: "financial",
|
|
916
916
|
role: "finance",
|
|
@@ -925,17 +925,6 @@ function ReceiptAgentSurface({ onOpenDraft }) {
|
|
|
925
925
|
return null;
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
-
// src/lib/idempotency.ts
|
|
929
|
-
function createIdempotencyKey(scope) {
|
|
930
|
-
let suffix;
|
|
931
|
-
try {
|
|
932
|
-
suffix = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
933
|
-
} catch {
|
|
934
|
-
suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
935
|
-
}
|
|
936
|
-
return `${scope}:${suffix}`;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
928
|
// src/lib/cnab240.ts
|
|
940
929
|
var onlyDigits = (s) => (s || "").replace(/\D/g, "");
|
|
941
930
|
var padL = (v, n, c = "0") => String(v ?? "").slice(0, n).padStart(n, c);
|
|
@@ -1404,22 +1393,22 @@ function lineFromInvoice(invoice, payee) {
|
|
|
1404
1393
|
var openAmount = (invoice) => Math.max(0, invoice.totalAmount - invoice.paidAmount);
|
|
1405
1394
|
function CnabRemittanceModal({ invoices, bankAccounts, provider, onClose }) {
|
|
1406
1395
|
const { currency } = useFinancialConfig();
|
|
1407
|
-
const [step, setStep] =
|
|
1408
|
-
const [bankAccountId, setBankAccountId] =
|
|
1409
|
-
const [draft, setDraft] =
|
|
1410
|
-
const [context, setContext] =
|
|
1411
|
-
const [companyName, setCompanyName] =
|
|
1412
|
-
const [companyDocument, setCompanyDocument] =
|
|
1413
|
-
const [lines, setLines] =
|
|
1414
|
-
const [companyUnsaved, setCompanyUnsaved] =
|
|
1415
|
-
const [loadingContext, setLoadingContext] =
|
|
1416
|
-
const [busy, setBusy] =
|
|
1417
|
-
const [error, setError] =
|
|
1396
|
+
const [step, setStep] = React29__default.useState(0);
|
|
1397
|
+
const [bankAccountId, setBankAccountId] = React29__default.useState("");
|
|
1398
|
+
const [draft, setDraft] = React29__default.useState(null);
|
|
1399
|
+
const [context, setContext] = React29__default.useState(null);
|
|
1400
|
+
const [companyName, setCompanyName] = React29__default.useState("");
|
|
1401
|
+
const [companyDocument, setCompanyDocument] = React29__default.useState("");
|
|
1402
|
+
const [lines, setLines] = React29__default.useState([]);
|
|
1403
|
+
const [companyUnsaved, setCompanyUnsaved] = React29__default.useState(false);
|
|
1404
|
+
const [loadingContext, setLoadingContext] = React29__default.useState(false);
|
|
1405
|
+
const [busy, setBusy] = React29__default.useState(false);
|
|
1406
|
+
const [error, setError] = React29__default.useState("");
|
|
1418
1407
|
const eligibleAccounts2 = bankAccounts.filter((account2) => account2.accountType === "bank" && account2.isActive);
|
|
1419
1408
|
const total = invoices.reduce((sum, invoice) => sum + openAmount(invoice), 0);
|
|
1420
1409
|
const account = eligibleAccounts2.find((candidate) => candidate.id === bankAccountId);
|
|
1421
1410
|
const missingBankData = draft ? missingFields(draft, companyDocument) : [];
|
|
1422
|
-
const items =
|
|
1411
|
+
const items = React29__default.useMemo(() => lines.map((line) => ({
|
|
1423
1412
|
reference: line.invoice.id,
|
|
1424
1413
|
amount: openAmount(line.invoice),
|
|
1425
1414
|
paymentDate: line.paymentDate,
|
|
@@ -1444,8 +1433,8 @@ function CnabRemittanceModal({ invoices, bankAccounts, provider, onClose }) {
|
|
|
1444
1433
|
postalCode: line.payee.postalCode
|
|
1445
1434
|
}
|
|
1446
1435
|
})), [lines]);
|
|
1447
|
-
const gaps =
|
|
1448
|
-
const batches =
|
|
1436
|
+
const gaps = React29__default.useMemo(() => cnabPayeeGaps(items), [items]);
|
|
1437
|
+
const batches = React29__default.useMemo(() => {
|
|
1449
1438
|
const grouped = /* @__PURE__ */ new Map();
|
|
1450
1439
|
for (const line of lines) {
|
|
1451
1440
|
const current = grouped.get(line.paymentForm) ?? { count: 0, total: 0 };
|
|
@@ -1861,6 +1850,376 @@ function Field({ label, value, onChange, required, placeholder, className = "" }
|
|
|
1861
1850
|
/* @__PURE__ */ jsx("input", { value, onChange: (event) => onChange(event.target.value), placeholder, className: "h-9 w-full rounded-input border bg-background px-3 text-sm outline-none focus:border-primary focus:ring-2 focus:ring-primary/15" })
|
|
1862
1851
|
] });
|
|
1863
1852
|
}
|
|
1853
|
+
|
|
1854
|
+
// src/lib/idempotency.ts
|
|
1855
|
+
function createIdempotencyKey(scope) {
|
|
1856
|
+
let suffix;
|
|
1857
|
+
try {
|
|
1858
|
+
suffix = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1859
|
+
} catch {
|
|
1860
|
+
suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1861
|
+
}
|
|
1862
|
+
return `${scope}:${suffix}`;
|
|
1863
|
+
}
|
|
1864
|
+
var DESTINATIONS = [
|
|
1865
|
+
{ value: "devolver", label: "Devolver ao fornecedor" },
|
|
1866
|
+
{ value: "perda", label: "Baixar como perda" },
|
|
1867
|
+
{ value: "manter", label: "Manter no estoque" }
|
|
1868
|
+
];
|
|
1869
|
+
function day(iso) {
|
|
1870
|
+
const [y, m, d] = iso.slice(0, 10).split("-");
|
|
1871
|
+
return `${d}/${m}/${y}`;
|
|
1872
|
+
}
|
|
1873
|
+
function CancelReasonFields({ reasons, reasonId, onReason, detail, onDetail }) {
|
|
1874
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1875
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Motivo do cancelamento" }),
|
|
1876
|
+
reasons === null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }) : /* @__PURE__ */ jsx(
|
|
1877
|
+
ChoiceSelect,
|
|
1878
|
+
{
|
|
1879
|
+
"aria-label": "Motivo do cancelamento",
|
|
1880
|
+
options: reasons.map((r) => ({ value: r.id, label: r.name })),
|
|
1881
|
+
value: reasonId,
|
|
1882
|
+
onChange: onReason,
|
|
1883
|
+
placeholder: "Selecione um motivo..."
|
|
1884
|
+
}
|
|
1885
|
+
),
|
|
1886
|
+
/* @__PURE__ */ jsx(
|
|
1887
|
+
"textarea",
|
|
1888
|
+
{
|
|
1889
|
+
value: detail,
|
|
1890
|
+
onChange: (e) => onDetail(e.target.value),
|
|
1891
|
+
placeholder: "Detalhe (opcional)...",
|
|
1892
|
+
className: "min-h-20 w-full resize-y rounded-input border border-input bg-card px-3 py-2 text-sm"
|
|
1893
|
+
}
|
|
1894
|
+
)
|
|
1895
|
+
] });
|
|
1896
|
+
}
|
|
1897
|
+
function CancelInvoiceDialog({ invoiceId, counterpartyName, totalAmount, items, preselectItemId, onClose, onCancelled }) {
|
|
1898
|
+
const { currency } = useFinancialConfig();
|
|
1899
|
+
const provider = useFinancialProvider();
|
|
1900
|
+
const [selected, setSelected] = useState(
|
|
1901
|
+
() => new Set(preselectItemId ? [preselectItemId] : items.map((i) => i.id))
|
|
1902
|
+
);
|
|
1903
|
+
const [reasons, setReasons] = useState(null);
|
|
1904
|
+
const [reasonId, setReasonId] = useState();
|
|
1905
|
+
const [detail, setDetail] = useState("");
|
|
1906
|
+
const [entries, setEntries] = useState(null);
|
|
1907
|
+
const [destinations, setDestinations] = useState({});
|
|
1908
|
+
const [busy, setBusy] = useState(false);
|
|
1909
|
+
const requestKey = useRef(createIdempotencyKey("financial:invoice-cancel-items"));
|
|
1910
|
+
useEffect(() => {
|
|
1911
|
+
let alive = true;
|
|
1912
|
+
Promise.all([
|
|
1913
|
+
provider.listCancellationReasons?.() ?? Promise.resolve([]),
|
|
1914
|
+
provider.getInvoiceStockEntries?.(invoiceId) ?? Promise.resolve([])
|
|
1915
|
+
]).then(([r, e]) => {
|
|
1916
|
+
if (alive) {
|
|
1917
|
+
setReasons(r);
|
|
1918
|
+
setEntries(e);
|
|
1919
|
+
}
|
|
1920
|
+
}).catch((err) => {
|
|
1921
|
+
if (alive) {
|
|
1922
|
+
setReasons([]);
|
|
1923
|
+
setEntries([]);
|
|
1924
|
+
toast.error("N\xE3o foi poss\xEDvel carregar o cancelamento", { description: err?.message });
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
return () => {
|
|
1928
|
+
alive = false;
|
|
1929
|
+
};
|
|
1930
|
+
}, [provider, invoiceId]);
|
|
1931
|
+
const whole = items.length > 0 && items.every((i) => selected.has(i.id));
|
|
1932
|
+
const amount = whole ? totalAmount : items.filter((i) => selected.has(i.id)).reduce((s, i) => s + i.totalAmount, 0);
|
|
1933
|
+
const stocked = useMemo(() => {
|
|
1934
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
1935
|
+
for (const e of entries ?? []) {
|
|
1936
|
+
const cur = byItem.get(e.itemId);
|
|
1937
|
+
byItem.set(e.itemId, { entered: (cur?.entered ?? 0) + e.entered, enteredOn: cur?.enteredOn ?? e.enteredOn });
|
|
1938
|
+
}
|
|
1939
|
+
return byItem;
|
|
1940
|
+
}, [entries]);
|
|
1941
|
+
const stockedSelected = items.filter((i) => selected.has(i.id) && stocked.has(i.id));
|
|
1942
|
+
const missing = selected.size === 0 ? "Selecione pelo menos um item" : stockedSelected.find((i) => !destinations[i.id]?.destination) ? "Diga o que acontece com a mercadoria" : !reasonId ? "Escolha o motivo do cancelamento" : null;
|
|
1943
|
+
function toggle(id, on) {
|
|
1944
|
+
setSelected((prev) => {
|
|
1945
|
+
const next = new Set(prev);
|
|
1946
|
+
if (on) next.add(id);
|
|
1947
|
+
else next.delete(id);
|
|
1948
|
+
return next;
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async function submit() {
|
|
1952
|
+
if (missing || !reasonId || !provider.cancelInvoiceItems) return;
|
|
1953
|
+
setBusy(true);
|
|
1954
|
+
try {
|
|
1955
|
+
const result = await provider.cancelInvoiceItems({
|
|
1956
|
+
invoiceId,
|
|
1957
|
+
itemIds: whole ? [] : [...selected],
|
|
1958
|
+
reasonId,
|
|
1959
|
+
detail,
|
|
1960
|
+
stock: stockedSelected.map((i) => ({
|
|
1961
|
+
itemId: i.id,
|
|
1962
|
+
destination: destinations[i.id].destination,
|
|
1963
|
+
note: destinations[i.id]?.note
|
|
1964
|
+
})),
|
|
1965
|
+
idempotencyKey: requestKey.current
|
|
1966
|
+
});
|
|
1967
|
+
if (result.creditAmount > 0) {
|
|
1968
|
+
toast.success(result.whole ? "Conta cancelada" : "Itens cancelados", {
|
|
1969
|
+
description: `${formatCurrency(result.creditAmount, currency)} em pagamentos ficaram como cr\xE9dito${counterpartyName ? ` de ${counterpartyName}` : ""}.`
|
|
1970
|
+
});
|
|
1971
|
+
} else {
|
|
1972
|
+
toast.success(result.whole ? "Conta cancelada" : "Itens cancelados");
|
|
1973
|
+
}
|
|
1974
|
+
onCancelled();
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
toast.error("N\xE3o foi poss\xEDvel cancelar", { description: err?.message });
|
|
1977
|
+
requestKey.current = createIdempotencyKey("financial:invoice-cancel-items");
|
|
1978
|
+
} finally {
|
|
1979
|
+
setBusy(false);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const loading = reasons === null || entries === null;
|
|
1983
|
+
return /* @__PURE__ */ jsxs(
|
|
1984
|
+
ModalTask,
|
|
1985
|
+
{
|
|
1986
|
+
open: true,
|
|
1987
|
+
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 text-destructive", children: [
|
|
1988
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4" }),
|
|
1989
|
+
" Cancelar"
|
|
1990
|
+
] }),
|
|
1991
|
+
width: "max-w-lg",
|
|
1992
|
+
onClose,
|
|
1993
|
+
children: [
|
|
1994
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
|
|
1995
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
1996
|
+
"Selecione o que cancelar. Pagamentos j\xE1 alocados ao que for cancelado ser\xE3o liberados como cr\xE9dito",
|
|
1997
|
+
counterpartyName ? ` de ${counterpartyName}` : "",
|
|
1998
|
+
"."
|
|
1999
|
+
] }),
|
|
2000
|
+
items.length > 1 && /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3 rounded-lg border border-destructive/30 bg-destructive-soft px-3 py-2.5 text-sm font-medium text-destructive", children: [
|
|
2001
|
+
/* @__PURE__ */ jsx(
|
|
2002
|
+
Checkbox,
|
|
2003
|
+
{
|
|
2004
|
+
checked: whole,
|
|
2005
|
+
indeterminate: !whole && selected.size > 0,
|
|
2006
|
+
color: "destructive",
|
|
2007
|
+
onChange: (on) => setSelected(on ? new Set(items.map((i) => i.id)) : /* @__PURE__ */ new Set())
|
|
2008
|
+
}
|
|
2009
|
+
),
|
|
2010
|
+
"Cancelar a conta inteira"
|
|
2011
|
+
] }),
|
|
2012
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: items.map((it) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2.5", children: [
|
|
2013
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: selected.has(it.id), onChange: (on) => toggle(it.id, on) }),
|
|
2014
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: it.description }),
|
|
2015
|
+
loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-16" }) : stocked.has(it.id) && /* @__PURE__ */ jsx(Badge, { variant: "success", className: "shrink-0", children: "No estoque" }),
|
|
2016
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(it.totalAmount, currency) })
|
|
2017
|
+
] }, it.id)) }),
|
|
2018
|
+
stockedSelected.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-4 rounded-lg border border-warning/40 bg-warning-soft p-3", children: [
|
|
2019
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-warning-soft-foreground", children: "Estes itens deram entrada no estoque. Diga o que acontece com a mercadoria: o destino vale para o saldo que ainda est\xE1 na posi\xE7\xE3o; o que j\xE1 foi consumido fica registrado." }),
|
|
2020
|
+
stockedSelected.map((it) => {
|
|
2021
|
+
const info = stocked.get(it.id);
|
|
2022
|
+
const choice = destinations[it.id];
|
|
2023
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2024
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2025
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: it.description }),
|
|
2026
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
2027
|
+
"Entrou no estoque: ",
|
|
2028
|
+
info.entered.toLocaleString("pt-BR"),
|
|
2029
|
+
" \xB7 ",
|
|
2030
|
+
day(info.enteredOn)
|
|
2031
|
+
] })
|
|
2032
|
+
] }),
|
|
2033
|
+
/* @__PURE__ */ jsx(
|
|
2034
|
+
SegmentedControl,
|
|
2035
|
+
{
|
|
2036
|
+
"aria-label": `Destino de ${it.description}`,
|
|
2037
|
+
options: DESTINATIONS,
|
|
2038
|
+
value: choice?.destination ?? "",
|
|
2039
|
+
onChange: (v) => setDestinations((d) => ({ ...d, [it.id]: { note: d[it.id]?.note ?? "", destination: v || void 0 } })),
|
|
2040
|
+
className: "flex-wrap"
|
|
2041
|
+
}
|
|
2042
|
+
),
|
|
2043
|
+
/* @__PURE__ */ jsx(
|
|
2044
|
+
"input",
|
|
2045
|
+
{
|
|
2046
|
+
value: choice?.note ?? "",
|
|
2047
|
+
onChange: (e) => setDestinations((d) => ({ ...d, [it.id]: { destination: d[it.id]?.destination, note: e.target.value } })),
|
|
2048
|
+
placeholder: "Observa\xE7\xE3o (ex.: nota de devolu\xE7\xE3o 8226400)",
|
|
2049
|
+
className: "h-9 w-full rounded-input border border-input bg-card px-3 text-sm"
|
|
2050
|
+
}
|
|
2051
|
+
)
|
|
2052
|
+
] }, it.id);
|
|
2053
|
+
})
|
|
2054
|
+
] }),
|
|
2055
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t pt-3 text-sm font-medium", children: [
|
|
2056
|
+
/* @__PURE__ */ jsx("span", { children: whole ? "Conta inteira ser\xE1 cancelada" : "Total a cancelar" }),
|
|
2057
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCurrency(amount, currency) })
|
|
2058
|
+
] }),
|
|
2059
|
+
/* @__PURE__ */ jsx(CancelReasonFields, { reasons, reasonId, onReason: setReasonId, detail, onDetail: setDetail })
|
|
2060
|
+
] }),
|
|
2061
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3", children: [
|
|
2062
|
+
missing && !loading && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: missing }),
|
|
2063
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: "Voltar" }),
|
|
2064
|
+
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: submit, disabled: busy || loading || !!missing, children: [
|
|
2065
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
|
|
2066
|
+
busy ? "Cancelando\u2026" : whole ? "Cancelar conta" : "Cancelar itens"
|
|
2067
|
+
] })
|
|
2068
|
+
] })
|
|
2069
|
+
]
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
function CancelInvoicesDialog({ targets, onClose, onCancelled }) {
|
|
2074
|
+
if (targets.length === 1) return /* @__PURE__ */ jsx(CancelOneFromList, { target: targets[0], onClose, onCancelled });
|
|
2075
|
+
return /* @__PURE__ */ jsx(CancelManyDialog, { targets, onClose, onCancelled });
|
|
2076
|
+
}
|
|
2077
|
+
function CancelOneFromList({ target, onClose, onCancelled }) {
|
|
2078
|
+
const provider = useFinancialProvider();
|
|
2079
|
+
const [loaded, setLoaded] = useState(null);
|
|
2080
|
+
useEffect(() => {
|
|
2081
|
+
let alive = true;
|
|
2082
|
+
Promise.all([provider.getInvoiceItems(target.id), provider.getInvoiceById(target.id)]).then(([items, invoice]) => {
|
|
2083
|
+
if (!alive) return;
|
|
2084
|
+
setLoaded({
|
|
2085
|
+
items: items.filter((i) => !i.cancelledAt),
|
|
2086
|
+
// The list row may be one installment; the dialog cancels the invoice.
|
|
2087
|
+
totalAmount: invoice?.totalAmount ?? target.totalAmount,
|
|
2088
|
+
counterpartyName: invoice?.contactName ?? target.counterpartyName
|
|
2089
|
+
});
|
|
2090
|
+
}).catch((err) => {
|
|
2091
|
+
if (alive) {
|
|
2092
|
+
toast.error("N\xE3o foi poss\xEDvel carregar o cancelamento", { description: err?.message });
|
|
2093
|
+
onClose();
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
return () => {
|
|
2097
|
+
alive = false;
|
|
2098
|
+
};
|
|
2099
|
+
}, [provider, target.id]);
|
|
2100
|
+
if (!loaded) {
|
|
2101
|
+
return /* @__PURE__ */ jsx(
|
|
2102
|
+
ModalTask,
|
|
2103
|
+
{
|
|
2104
|
+
open: true,
|
|
2105
|
+
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 text-destructive", children: [
|
|
2106
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4" }),
|
|
2107
|
+
" Cancelar"
|
|
2108
|
+
] }),
|
|
2109
|
+
width: "max-w-lg",
|
|
2110
|
+
onClose,
|
|
2111
|
+
children: /* @__PURE__ */ jsxs("div", { className: "space-y-3 px-5 py-4", children: [
|
|
2112
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
|
|
2113
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }),
|
|
2114
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-20 w-full" })
|
|
2115
|
+
] })
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
return /* @__PURE__ */ jsx(
|
|
2120
|
+
CancelInvoiceDialog,
|
|
2121
|
+
{
|
|
2122
|
+
invoiceId: target.id,
|
|
2123
|
+
counterpartyName: loaded.counterpartyName,
|
|
2124
|
+
totalAmount: loaded.totalAmount,
|
|
2125
|
+
items: loaded.items,
|
|
2126
|
+
onClose,
|
|
2127
|
+
onCancelled
|
|
2128
|
+
}
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
function CancelManyDialog({ targets, onClose, onCancelled }) {
|
|
2132
|
+
const { currency } = useFinancialConfig();
|
|
2133
|
+
const provider = useFinancialProvider();
|
|
2134
|
+
const [reasons, setReasons] = useState(null);
|
|
2135
|
+
const [reasonId, setReasonId] = useState();
|
|
2136
|
+
const [detail, setDetail] = useState("");
|
|
2137
|
+
const [busy, setBusy] = useState(false);
|
|
2138
|
+
const requestKey = useRef(createIdempotencyKey("financial:invoice-bulk-cancel"));
|
|
2139
|
+
useEffect(() => {
|
|
2140
|
+
let alive = true;
|
|
2141
|
+
(provider.listCancellationReasons?.() ?? Promise.resolve([])).then((r) => {
|
|
2142
|
+
if (alive) setReasons(r);
|
|
2143
|
+
}).catch((err) => {
|
|
2144
|
+
if (alive) {
|
|
2145
|
+
setReasons([]);
|
|
2146
|
+
toast.error("N\xE3o foi poss\xEDvel carregar o cancelamento", { description: err?.message });
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
return () => {
|
|
2150
|
+
alive = false;
|
|
2151
|
+
};
|
|
2152
|
+
}, [provider]);
|
|
2153
|
+
const total = targets.reduce((s, x) => s + x.totalAmount, 0);
|
|
2154
|
+
async function submit() {
|
|
2155
|
+
if (!reasonId) return;
|
|
2156
|
+
setBusy(true);
|
|
2157
|
+
let done = 0;
|
|
2158
|
+
let failed = 0;
|
|
2159
|
+
let firstError;
|
|
2160
|
+
const reasonName = reasons?.find((r) => r.id === reasonId)?.name ?? "";
|
|
2161
|
+
for (const target of targets) {
|
|
2162
|
+
const idempotencyKey = `${requestKey.current}:${target.id}`;
|
|
2163
|
+
try {
|
|
2164
|
+
if (provider.cancelInvoiceItems) {
|
|
2165
|
+
await provider.cancelInvoiceItems({ invoiceId: target.id, itemIds: [], reasonId, detail, stock: [], idempotencyKey });
|
|
2166
|
+
} else {
|
|
2167
|
+
await provider.cancelInvoice({ invoiceId: target.id, reason: [reasonName, detail.trim()].filter(Boolean).join(" \u2014 "), idempotencyKey });
|
|
2168
|
+
}
|
|
2169
|
+
done++;
|
|
2170
|
+
} catch (err) {
|
|
2171
|
+
failed++;
|
|
2172
|
+
firstError ?? (firstError = err?.message);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
setBusy(false);
|
|
2176
|
+
if (done > 0) toast.success(done === 1 ? "1 conta cancelada" : `${done} contas canceladas`);
|
|
2177
|
+
if (failed > 0) {
|
|
2178
|
+
toast.error(failed === 1 ? "1 conta n\xE3o p\xF4de ser cancelada" : `${failed} contas n\xE3o puderam ser canceladas`, {
|
|
2179
|
+
description: firstError ?? "Abra a conta para cancelar: ela pode ter mercadoria no estoque."
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
onCancelled();
|
|
2183
|
+
}
|
|
2184
|
+
return /* @__PURE__ */ jsxs(
|
|
2185
|
+
ModalTask,
|
|
2186
|
+
{
|
|
2187
|
+
open: true,
|
|
2188
|
+
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 text-destructive", children: [
|
|
2189
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4" }),
|
|
2190
|
+
" Cancelar"
|
|
2191
|
+
] }),
|
|
2192
|
+
width: "max-w-lg",
|
|
2193
|
+
onClose,
|
|
2194
|
+
children: [
|
|
2195
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
|
|
2196
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
2197
|
+
"As ",
|
|
2198
|
+
targets.length,
|
|
2199
|
+
" contas abaixo ser\xE3o canceladas por inteiro. Pagamentos j\xE1 alocados a elas ser\xE3o liberados como cr\xE9dito."
|
|
2200
|
+
] }),
|
|
2201
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-48 space-y-2 overflow-y-auto", children: targets.map((x) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-lg border px-3 py-2.5", children: [
|
|
2202
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: x.label }),
|
|
2203
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(x.totalAmount, currency) })
|
|
2204
|
+
] }, x.id)) }),
|
|
2205
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t pt-3 text-sm font-medium", children: [
|
|
2206
|
+
/* @__PURE__ */ jsx("span", { children: "Total a cancelar" }),
|
|
2207
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCurrency(total, currency) })
|
|
2208
|
+
] }),
|
|
2209
|
+
/* @__PURE__ */ jsx(CancelReasonFields, { reasons, reasonId, onReason: setReasonId, detail, onDetail: setDetail })
|
|
2210
|
+
] }),
|
|
2211
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3", children: [
|
|
2212
|
+
!reasonId && reasons !== null && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: "Escolha o motivo do cancelamento" }),
|
|
2213
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: "Voltar" }),
|
|
2214
|
+
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: submit, disabled: busy || reasons === null || !reasonId, children: [
|
|
2215
|
+
/* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
|
|
2216
|
+
busy ? "Cancelando\u2026" : "Cancelar contas"
|
|
2217
|
+
] })
|
|
2218
|
+
] })
|
|
2219
|
+
]
|
|
2220
|
+
}
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
1864
2223
|
var ACCEPT = "image/*,application/pdf";
|
|
1865
2224
|
var MAX_BYTES = 15 * 1024 * 1024;
|
|
1866
2225
|
function size(bytes) {
|
|
@@ -2066,7 +2425,7 @@ function FinancialCounterpartySearchSelect({
|
|
|
2066
2425
|
onEdit,
|
|
2067
2426
|
disabled
|
|
2068
2427
|
}) {
|
|
2069
|
-
const search =
|
|
2428
|
+
const search = React29.useCallback(async (query) => {
|
|
2070
2429
|
const results = await lookup.search(query);
|
|
2071
2430
|
return results.map((result) => ({
|
|
2072
2431
|
id: result.id,
|
|
@@ -2196,12 +2555,12 @@ function useInvoiceColumns(currency, paymentMethods2, paymentMethodTypes, bankAc
|
|
|
2196
2555
|
accessorKey: "totalAmount",
|
|
2197
2556
|
header: t2("financial.invoice.columnAmount"),
|
|
2198
2557
|
enableSorting: true,
|
|
2199
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `whitespace-nowrap font-medium ${row.original.status === "cancelled" ? "line-through text-muted-foreground" : ""}`, children: formatCurrency(row.original.totalAmount, currency) })
|
|
2558
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `whitespace-nowrap font-medium ${row.original.status === "cancelled" ? "line-through text-muted-foreground" : ""}`, children: formatCurrency(row.original.totalAmount, resolveCurrency(currency, row.original.currency)) })
|
|
2200
2559
|
},
|
|
2201
2560
|
{
|
|
2202
2561
|
accessorKey: "paidAmount",
|
|
2203
2562
|
header: t2("financial.invoice.columnPaid"),
|
|
2204
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `whitespace-nowrap ${row.original.status === "cancelled" ? "line-through text-muted-foreground" : "text-muted-foreground"}`, children: formatCurrency(row.original.paidAmount, currency) })
|
|
2563
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `whitespace-nowrap ${row.original.status === "cancelled" ? "line-through text-muted-foreground" : "text-muted-foreground"}`, children: formatCurrency(row.original.paidAmount, resolveCurrency(currency, row.original.currency)) })
|
|
2205
2564
|
},
|
|
2206
2565
|
{
|
|
2207
2566
|
id: "paymentMethod",
|
|
@@ -2429,52 +2788,44 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
2429
2788
|
() => STATUS_OPTIONS.filter((o) => o.value !== "provisioned").map((o) => ({ value: o.value, label: t2(o.labelKey) })),
|
|
2430
2789
|
[t2]
|
|
2431
2790
|
);
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
let paidAmount = 0;
|
|
2435
|
-
let openAmount2 = 0;
|
|
2791
|
+
const summaries = useMemo(() => {
|
|
2792
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
2436
2793
|
for (const inv of filtered) {
|
|
2437
2794
|
if (inv.status === "cancelled") continue;
|
|
2438
|
-
|
|
2439
|
-
|
|
2795
|
+
const cur = resolveCurrency(currency, inv.currency);
|
|
2796
|
+
const sum = byCode.get(cur.code) ?? { currency: cur, totalAmount: 0, paidAmount: 0, openAmount: 0 };
|
|
2797
|
+
sum.totalAmount += inv.totalAmount;
|
|
2798
|
+
sum.paidAmount += inv.paidAmount;
|
|
2799
|
+
sum.openAmount = sum.totalAmount - sum.paidAmount;
|
|
2800
|
+
byCode.set(cur.code, sum);
|
|
2440
2801
|
}
|
|
2441
|
-
|
|
2442
|
-
return { totalAmount, paidAmount, openAmount: openAmount2 };
|
|
2802
|
+
return [...byCode.values()];
|
|
2443
2803
|
}, [filtered]);
|
|
2444
2804
|
const [cancelTarget, setCancelTarget] = useState(null);
|
|
2445
2805
|
const [cnabTarget, setCnabTarget] = useState(null);
|
|
2446
|
-
const
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
const targets = (cancelTarget ?? []).filter((inv) => inv.status !== "cancelled");
|
|
2456
|
-
setBulkBusy(true);
|
|
2457
|
-
let done = 0;
|
|
2458
|
-
let failed = 0;
|
|
2459
|
-
for (const invoice of targets) {
|
|
2460
|
-
try {
|
|
2461
|
-
await provider.cancelInvoice({
|
|
2462
|
-
invoiceId: invoice.invoiceId ?? invoice.id,
|
|
2463
|
-
reason: cancelReason,
|
|
2464
|
-
idempotencyKey: `${cancelRequestKey.current}:${invoice.invoiceId ?? invoice.id}`
|
|
2465
|
-
});
|
|
2466
|
-
done++;
|
|
2467
|
-
} catch (err) {
|
|
2468
|
-
failed++;
|
|
2469
|
-
console.warn("[financial] cancelInvoice failed", invoice.id, err);
|
|
2806
|
+
const closeBulkCancel = useCallback(() => setCancelTarget(null), []);
|
|
2807
|
+
const cancelTargets = useMemo(() => {
|
|
2808
|
+
const byInvoice = /* @__PURE__ */ new Map();
|
|
2809
|
+
for (const inv of cancelTarget ?? []) {
|
|
2810
|
+
const id = inv.invoiceId ?? inv.id;
|
|
2811
|
+
const seen = byInvoice.get(id);
|
|
2812
|
+
if (seen) {
|
|
2813
|
+
seen.totalAmount += inv.totalAmount;
|
|
2814
|
+
continue;
|
|
2470
2815
|
}
|
|
2816
|
+
byInvoice.set(id, {
|
|
2817
|
+
id,
|
|
2818
|
+
label: [inv.contactName, inv.itemsSummary].filter(Boolean).join(" \xB7 ") || inv.fiscalNumber || id,
|
|
2819
|
+
counterpartyName: inv.contactName,
|
|
2820
|
+
totalAmount: inv.totalAmount
|
|
2821
|
+
});
|
|
2471
2822
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2823
|
+
return [...byInvoice.values()];
|
|
2824
|
+
}, [cancelTarget]);
|
|
2825
|
+
const afterBulkCancel = useCallback(async () => {
|
|
2826
|
+
setCancelTarget(null);
|
|
2474
2827
|
await Promise.all([fetchInvoices(query), fetchSummary()]);
|
|
2475
|
-
|
|
2476
|
-
if (failed > 0) toast.error(t2("financial.invoice.bulkCancelFailed", { count: String(failed) }));
|
|
2477
|
-
}, [cancelTarget, cancelReason, closeBulkCancel, provider, fetchInvoices, fetchSummary, query, t2]);
|
|
2828
|
+
}, [fetchInvoices, fetchSummary, query]);
|
|
2478
2829
|
const exportSelected = useCallback((rows) => {
|
|
2479
2830
|
exportCSV(
|
|
2480
2831
|
[
|
|
@@ -2492,7 +2843,7 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
2492
2843
|
}, [direction, t2]);
|
|
2493
2844
|
const printSelected = useCallback((rows) => {
|
|
2494
2845
|
const safe = (value) => String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character] ?? character);
|
|
2495
|
-
const body = rows.map((row) => `<tr><td>${safe(row.dueDate)}</td><td>${safe(row.contactName)}<small>${safe(row.itemsSummary)}</small></td><td>${safe(formatCurrency(row.totalAmount, currency))}</td><td>${row.installmentNumber ?? 1}/${row.totalInstallments ?? 1}</td></tr>`).join("");
|
|
2846
|
+
const body = rows.map((row) => `<tr><td>${safe(row.dueDate)}</td><td>${safe(row.contactName)}<small>${safe(row.itemsSummary)}</small></td><td>${safe(formatCurrency(row.totalAmount, resolveCurrency(currency, row.currency)))}</td><td>${row.installmentNumber ?? 1}/${row.totalInstallments ?? 1}</td></tr>`).join("");
|
|
2496
2847
|
const title = direction === "debit" ? "Contas a pagar" : "Contas a receber";
|
|
2497
2848
|
const popup = printDocument({
|
|
2498
2849
|
title,
|
|
@@ -2519,7 +2870,6 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
2519
2870
|
label: t2("financial.invoice.bulkCancel"),
|
|
2520
2871
|
icon: Ban,
|
|
2521
2872
|
destructive: true,
|
|
2522
|
-
disabled: bulkBusy,
|
|
2523
2873
|
onSelect: (rows) => {
|
|
2524
2874
|
const targets = rows.filter((inv) => inv.status !== "cancelled");
|
|
2525
2875
|
if (targets.length === 0) {
|
|
@@ -2530,7 +2880,7 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
2530
2880
|
}
|
|
2531
2881
|
});
|
|
2532
2882
|
return actions;
|
|
2533
|
-
}, [canEdit,
|
|
2883
|
+
}, [canEdit, direction, exportSelected, printSelected, t2]);
|
|
2534
2884
|
const receivedLabel = direction === "debit" ? t2("financial.invoice.summaryPaid") : t2("financial.invoice.summaryReceived");
|
|
2535
2885
|
const pendingLabel = direction === "debit" ? t2("financial.invoice.summaryPayable") : t2("financial.invoice.summaryReceivable");
|
|
2536
2886
|
const exportRows = () => {
|
|
@@ -2840,53 +3190,24 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
2840
3190
|
}
|
|
2841
3191
|
}
|
|
2842
3192
|
),
|
|
2843
|
-
filtered.length > 0 && !invoicesLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-6 rounded-lg border bg-muted/30 px-5 py-3 text-sm", children: [
|
|
3193
|
+
filtered.length > 0 && !invoicesLoading && summaries.map((summary) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-end gap-x-6 gap-y-1 rounded-lg border bg-muted/30 px-5 py-3 text-sm", children: [
|
|
3194
|
+
summaries.length > 1 && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs font-medium text-muted-foreground", children: summary.currency.code }),
|
|
2844
3195
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2845
3196
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("financial.invoice.summaryTotal") }),
|
|
2846
|
-
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCurrency(summary.totalAmount, currency) })
|
|
3197
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCurrency(summary.totalAmount, summary.currency) })
|
|
2847
3198
|
] }),
|
|
2848
3199
|
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-border" }),
|
|
2849
3200
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2850
3201
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: receivedLabel }),
|
|
2851
|
-
/* @__PURE__ */ jsx("span", { className: "font-semibold text-success", children: formatCurrency(summary.paidAmount, currency) })
|
|
3202
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-success", children: formatCurrency(summary.paidAmount, summary.currency) })
|
|
2852
3203
|
] }),
|
|
2853
3204
|
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-border" }),
|
|
2854
3205
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2855
3206
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: pendingLabel }),
|
|
2856
|
-
/* @__PURE__ */ jsx("span", { className: "font-semibold text-warning", children: formatCurrency(summary.openAmount, currency) })
|
|
3207
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-warning", children: formatCurrency(summary.openAmount, summary.currency) })
|
|
2857
3208
|
] })
|
|
2858
|
-
] }),
|
|
2859
|
-
cancelTarget !== null &&
|
|
2860
|
-
/* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-[60] flex items-center justify-center", children: [
|
|
2861
|
-
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-[2px]", onClick: closeBulkCancel }),
|
|
2862
|
-
/* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-w-sm rounded-modal border bg-card p-6 shadow-lg mx-4", role: "alertdialog", "aria-modal": "true", children: [
|
|
2863
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
2864
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 shrink-0", children: /* @__PURE__ */ jsx(Ban, { className: "h-5 w-5 text-destructive" }) }),
|
|
2865
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
2866
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.invoice.bulkCancelTitle", { count: String(cancelTarget.length) }) }),
|
|
2867
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoice.cancelDesc") })
|
|
2868
|
-
] })
|
|
2869
|
-
] }),
|
|
2870
|
-
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-bulk-cancel-reason", children: t2("financial.invoice.cancelReason") }),
|
|
2871
|
-
/* @__PURE__ */ jsx(
|
|
2872
|
-
"textarea",
|
|
2873
|
-
{
|
|
2874
|
-
id: "invoice-bulk-cancel-reason",
|
|
2875
|
-
value: cancelReason,
|
|
2876
|
-
onChange: (event) => setCancelReason(event.target.value),
|
|
2877
|
-
placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
|
|
2878
|
-
className: "mb-5 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
|
|
2879
|
-
autoFocus: true
|
|
2880
|
-
}
|
|
2881
|
-
),
|
|
2882
|
-
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
2883
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeBulkCancel, disabled: bulkBusy, children: t2("financial.invoice.keepInvoice") }),
|
|
2884
|
-
/* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: () => void confirmBulkCancel(), disabled: bulkBusy || !cancelReason.trim(), children: t2("financial.invoice.yesCancel") })
|
|
2885
|
-
] })
|
|
2886
|
-
] })
|
|
2887
|
-
] }),
|
|
2888
|
-
document.body
|
|
2889
|
-
),
|
|
3209
|
+
] }, summary.currency.code)),
|
|
3210
|
+
cancelTarget !== null && cancelTargets.length > 0 && /* @__PURE__ */ jsx(CancelInvoicesDialog, { targets: cancelTargets, onClose: closeBulkCancel, onCancelled: () => void afterBulkCancel() }),
|
|
2890
3211
|
cnabTarget !== null ? /* @__PURE__ */ jsx(CnabRemittanceModal, { invoices: cnabTarget, bankAccounts, provider, onClose: () => setCnabTarget(null) }) : null
|
|
2891
3212
|
] });
|
|
2892
3213
|
}
|
|
@@ -3955,9 +4276,9 @@ var SUBMIT_DRAFT = {
|
|
|
3955
4276
|
parameters: { type: "object", properties: {} }
|
|
3956
4277
|
};
|
|
3957
4278
|
function PayableDraftSurface({ config, draft }) {
|
|
3958
|
-
const live =
|
|
4279
|
+
const live = React29.useRef(draft);
|
|
3959
4280
|
live.current = draft;
|
|
3960
|
-
const lookup =
|
|
4281
|
+
const lookup = React29.useMemo(
|
|
3961
4282
|
() => config.contactLookup ?? createArchetypeLookup({
|
|
3962
4283
|
archetype: "person",
|
|
3963
4284
|
kind: "supplier",
|
|
@@ -3966,7 +4287,7 @@ function PayableDraftSurface({ config, draft }) {
|
|
|
3966
4287
|
}),
|
|
3967
4288
|
[config.contactLookup]
|
|
3968
4289
|
);
|
|
3969
|
-
|
|
4290
|
+
React29.useEffect(() => {
|
|
3970
4291
|
liveDraft = {
|
|
3971
4292
|
describe: () => live.current.describe(),
|
|
3972
4293
|
setSupplier: (name, id) => live.current.setSupplier(name, id),
|
|
@@ -3977,7 +4298,7 @@ function PayableDraftSurface({ config, draft }) {
|
|
|
3977
4298
|
liveDraft = null;
|
|
3978
4299
|
};
|
|
3979
4300
|
}, []);
|
|
3980
|
-
const contribution =
|
|
4301
|
+
const contribution = React29.useMemo(() => ({
|
|
3981
4302
|
id: "financial.payables.draft",
|
|
3982
4303
|
pluginId: "financial",
|
|
3983
4304
|
role: "finance",
|
|
@@ -4052,7 +4373,7 @@ function PayableDraftSurface({ config, draft }) {
|
|
|
4052
4373
|
}
|
|
4053
4374
|
function createFinancialAgentMount(deps) {
|
|
4054
4375
|
const FinancialAgentMount = () => {
|
|
4055
|
-
const contribution =
|
|
4376
|
+
const contribution = React29.useMemo(() => ({
|
|
4056
4377
|
id: "financial.payables.global",
|
|
4057
4378
|
pluginId: "financial",
|
|
4058
4379
|
role: "finance",
|
|
@@ -4562,10 +4883,10 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
4562
4883
|
const payMovement = useFinancialStore((s) => s.payMovement);
|
|
4563
4884
|
const remaining = movement.amount - movement.paidAmount;
|
|
4564
4885
|
const [saving, setSaving] = useState(false);
|
|
4565
|
-
const command =
|
|
4886
|
+
const command = React29__default.useRef(createPendingCommand(() => createIdempotencyKey("financial:payment"), (input) => input.movementId));
|
|
4566
4887
|
const [pending2, setPending] = useState(false);
|
|
4567
4888
|
const [visible, setVisible] = useState(false);
|
|
4568
|
-
|
|
4889
|
+
React29__default.useEffect(() => {
|
|
4569
4890
|
requestAnimationFrame(() => setVisible(true));
|
|
4570
4891
|
}, []);
|
|
4571
4892
|
const source = movement.metadata ?? {};
|
|
@@ -4779,8 +5100,8 @@ function InstallmentSourceFields({
|
|
|
4779
5100
|
}
|
|
4780
5101
|
var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
4781
5102
|
function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel }) {
|
|
4782
|
-
const [open, setOpen] =
|
|
4783
|
-
const [q, setQ] =
|
|
5103
|
+
const [open, setOpen] = React29__default.useState(false);
|
|
5104
|
+
const [q, setQ] = React29__default.useState("");
|
|
4784
5105
|
const selected = accounts.find((a) => a.id === value);
|
|
4785
5106
|
const list = q ? accounts.filter((a) => norm2(a.name).includes(norm2(q))) : accounts;
|
|
4786
5107
|
const pick = (id) => {
|
|
@@ -4881,9 +5202,44 @@ function InstallmentStatusBadge({ status }) {
|
|
|
4881
5202
|
t2(s.labelKey)
|
|
4882
5203
|
] });
|
|
4883
5204
|
}
|
|
5205
|
+
function InlineAmount({ label, value, onCommit }) {
|
|
5206
|
+
const [draft, setDraft] = React29__default.useState(null);
|
|
5207
|
+
if (draft === null) {
|
|
5208
|
+
return /* @__PURE__ */ jsx(
|
|
5209
|
+
"button",
|
|
5210
|
+
{
|
|
5211
|
+
type: "button",
|
|
5212
|
+
onClick: (e) => {
|
|
5213
|
+
e.stopPropagation();
|
|
5214
|
+
setDraft(value);
|
|
5215
|
+
},
|
|
5216
|
+
className: `shrink-0 text-sm font-medium tabular-nums hover:opacity-80 ${INLINE_LINK_TONE.dashed}`,
|
|
5217
|
+
children: label
|
|
5218
|
+
}
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
const commit = () => {
|
|
5222
|
+
setDraft(null);
|
|
5223
|
+
if (Math.round(draft * 100) !== Math.round(value * 100)) onCommit(draft);
|
|
5224
|
+
};
|
|
5225
|
+
return /* @__PURE__ */ jsx(
|
|
5226
|
+
"div",
|
|
5227
|
+
{
|
|
5228
|
+
onClick: (e) => e.stopPropagation(),
|
|
5229
|
+
onBlur: commit,
|
|
5230
|
+
onKeyDown: (e) => {
|
|
5231
|
+
if (e.key === "Enter") commit();
|
|
5232
|
+
if (e.key === "Escape") setDraft(null);
|
|
5233
|
+
},
|
|
5234
|
+
children: /* @__PURE__ */ jsx(CurrencyInput, { autoFocus: true, value: draft, onChange: setDraft, className: "w-36" })
|
|
5235
|
+
}
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
4884
5238
|
function InstallmentRow({
|
|
4885
5239
|
number,
|
|
4886
5240
|
amount,
|
|
5241
|
+
amountValue,
|
|
5242
|
+
onAmountChange,
|
|
4887
5243
|
status,
|
|
4888
5244
|
dueDate,
|
|
4889
5245
|
onDueDateChange,
|
|
@@ -4906,7 +5262,7 @@ function InstallmentRow({
|
|
|
4906
5262
|
] }),
|
|
4907
5263
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col items-start gap-0.5", children: [
|
|
4908
5264
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4909
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-medium tabular-nums", children: amount }),
|
|
5265
|
+
onAmountChange && amountValue !== void 0 ? /* @__PURE__ */ jsx(InlineAmount, { label: amount, value: amountValue, onCommit: onAmountChange }) : /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-medium tabular-nums", children: amount }),
|
|
4910
5266
|
status && /* @__PURE__ */ jsx(InstallmentStatusBadge, { status })
|
|
4911
5267
|
] }),
|
|
4912
5268
|
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1 whitespace-nowrap text-[10px] text-muted-foreground", onClick: stop, children: [
|
|
@@ -4964,212 +5320,142 @@ function InstallmentReceiptButton({ onClick, title, attached, pending: pending2,
|
|
|
4964
5320
|
}
|
|
4965
5321
|
);
|
|
4966
5322
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5323
|
+
|
|
5324
|
+
// src/lib/installment-amounts.ts
|
|
5325
|
+
var cents = (v) => Math.round(v * 100);
|
|
5326
|
+
function rebalanceInstallments(input) {
|
|
5327
|
+
const { amounts, index, total } = input;
|
|
5328
|
+
const locked = input.locked ?? [];
|
|
5329
|
+
const pinned = input.pinned ?? [];
|
|
5330
|
+
if (locked[index]) return null;
|
|
5331
|
+
const open = amounts.map((_, i) => i).filter((i) => i !== index && !locked[i]);
|
|
5332
|
+
if (open.length === 0) return null;
|
|
5333
|
+
const free = open.filter((i) => !pinned[i]);
|
|
5334
|
+
const absorbers = free.length > 0 ? free : open;
|
|
5335
|
+
const fixed = amounts.reduce((sum, a, i) => i === index || absorbers.includes(i) ? sum : sum + cents(a), 0);
|
|
5336
|
+
const room = cents(total) - fixed;
|
|
5337
|
+
if (room < 0) return null;
|
|
5338
|
+
const mine = Math.min(Math.max(cents(input.value), 0), room);
|
|
5339
|
+
const rest = room - mine;
|
|
5340
|
+
const each = Math.floor(rest / absorbers.length);
|
|
5341
|
+
const next = amounts.map(cents);
|
|
5342
|
+
next[index] = mine;
|
|
5343
|
+
absorbers.forEach((i, n) => {
|
|
5344
|
+
next[i] = each + (n === 0 ? rest - each * absorbers.length : 0);
|
|
5345
|
+
});
|
|
5346
|
+
return next.map((c) => c / 100);
|
|
5347
|
+
}
|
|
5348
|
+
var day2 = (iso) => {
|
|
5349
|
+
if (!iso) return "";
|
|
4973
5350
|
const [y, m, d] = iso.slice(0, 10).split("-");
|
|
4974
5351
|
return `${d}/${m}/${y}`;
|
|
5352
|
+
};
|
|
5353
|
+
var qty = (n) => n.toLocaleString("pt-BR");
|
|
5354
|
+
var real = (name) => name && name !== "Padr\xE3o" ? name : void 0;
|
|
5355
|
+
function StockEntryNote({ entries, onOpen }) {
|
|
5356
|
+
if (!entries || entries.length === 0) return null;
|
|
5357
|
+
const total = entries.reduce((sum, e) => sum + e.quantity, 0);
|
|
5358
|
+
const named = entries.filter((e) => real(e.variantName));
|
|
5359
|
+
const first = entries.map((e) => e.enteredOn).filter(Boolean).sort()[0];
|
|
5360
|
+
const text2 = /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
5361
|
+
"Entrou no estoque: ",
|
|
5362
|
+
qty(total),
|
|
5363
|
+
named.length > 0 && ` (${named.map((e) => `${qty(e.quantity)} ${e.variantName}`).join(", ")})`,
|
|
5364
|
+
first && ` \xB7 ${day2(first)}`
|
|
5365
|
+
] });
|
|
5366
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5367
|
+
/* @__PURE__ */ jsx(Package, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
|
|
5368
|
+
text2
|
|
5369
|
+
] });
|
|
5370
|
+
if (!onOpen) return /* @__PURE__ */ jsx("span", { className: "inline-flex max-w-full items-center gap-1 text-success", children: body });
|
|
5371
|
+
return /* @__PURE__ */ jsx(
|
|
5372
|
+
"button",
|
|
5373
|
+
{
|
|
5374
|
+
type: "button",
|
|
5375
|
+
onClick: (e) => {
|
|
5376
|
+
e.stopPropagation();
|
|
5377
|
+
onOpen();
|
|
5378
|
+
},
|
|
5379
|
+
className: `inline-flex max-w-full items-center gap-1 text-success hover:opacity-80 ${INLINE_LINK_TONE.dashed}`,
|
|
5380
|
+
children: body
|
|
5381
|
+
}
|
|
5382
|
+
);
|
|
4975
5383
|
}
|
|
4976
|
-
function
|
|
5384
|
+
function StockEntryDetailsDialog({ title, entries, onClose }) {
|
|
4977
5385
|
const { currency } = useFinancialConfig();
|
|
4978
|
-
const
|
|
4979
|
-
const
|
|
4980
|
-
|
|
4981
|
-
);
|
|
4982
|
-
const
|
|
4983
|
-
const
|
|
4984
|
-
const
|
|
4985
|
-
|
|
4986
|
-
const [destinations, setDestinations] = useState({});
|
|
4987
|
-
const [busy, setBusy] = useState(false);
|
|
4988
|
-
const requestKey = useRef(createIdempotencyKey("financial:invoice-cancel-items"));
|
|
4989
|
-
useEffect(() => {
|
|
4990
|
-
let alive = true;
|
|
4991
|
-
Promise.all([
|
|
4992
|
-
provider.listCancellationReasons?.() ?? Promise.resolve([]),
|
|
4993
|
-
provider.getInvoiceStockEntries?.(invoiceId) ?? Promise.resolve([])
|
|
4994
|
-
]).then(([r, e]) => {
|
|
4995
|
-
if (alive) {
|
|
4996
|
-
setReasons(r);
|
|
4997
|
-
setEntries(e);
|
|
4998
|
-
}
|
|
4999
|
-
}).catch((err) => {
|
|
5000
|
-
if (alive) {
|
|
5001
|
-
setReasons([]);
|
|
5002
|
-
setEntries([]);
|
|
5003
|
-
toast.error("N\xE3o foi poss\xEDvel carregar o cancelamento", { description: err?.message });
|
|
5004
|
-
}
|
|
5005
|
-
});
|
|
5006
|
-
return () => {
|
|
5007
|
-
alive = false;
|
|
5008
|
-
};
|
|
5009
|
-
}, [provider, invoiceId]);
|
|
5010
|
-
const whole = items.length > 0 && items.every((i) => selected.has(i.id));
|
|
5011
|
-
const amount = whole ? totalAmount : items.filter((i) => selected.has(i.id)).reduce((s, i) => s + i.totalAmount, 0);
|
|
5012
|
-
const stocked = useMemo(() => {
|
|
5013
|
-
const byItem = /* @__PURE__ */ new Map();
|
|
5014
|
-
for (const e of entries ?? []) {
|
|
5015
|
-
const cur = byItem.get(e.itemId);
|
|
5016
|
-
byItem.set(e.itemId, { entered: (cur?.entered ?? 0) + e.entered, enteredOn: cur?.enteredOn ?? e.enteredOn });
|
|
5017
|
-
}
|
|
5018
|
-
return byItem;
|
|
5019
|
-
}, [entries]);
|
|
5020
|
-
const stockedSelected = items.filter((i) => selected.has(i.id) && stocked.has(i.id));
|
|
5021
|
-
const missing = selected.size === 0 ? "Selecione pelo menos um item" : stockedSelected.find((i) => !destinations[i.id]?.destination) ? "Diga o que acontece com a mercadoria" : !reasonId ? "Escolha o motivo do cancelamento" : null;
|
|
5022
|
-
function toggle(id, on) {
|
|
5023
|
-
setSelected((prev) => {
|
|
5024
|
-
const next = new Set(prev);
|
|
5025
|
-
if (on) next.add(id);
|
|
5026
|
-
else next.delete(id);
|
|
5027
|
-
return next;
|
|
5028
|
-
});
|
|
5029
|
-
}
|
|
5030
|
-
async function submit() {
|
|
5031
|
-
if (missing || !reasonId || !provider.cancelInvoiceItems) return;
|
|
5032
|
-
setBusy(true);
|
|
5033
|
-
try {
|
|
5034
|
-
const result = await provider.cancelInvoiceItems({
|
|
5035
|
-
invoiceId,
|
|
5036
|
-
itemIds: whole ? [] : [...selected],
|
|
5037
|
-
reasonId,
|
|
5038
|
-
detail,
|
|
5039
|
-
stock: stockedSelected.map((i) => ({
|
|
5040
|
-
itemId: i.id,
|
|
5041
|
-
destination: destinations[i.id].destination,
|
|
5042
|
-
note: destinations[i.id]?.note
|
|
5043
|
-
})),
|
|
5044
|
-
idempotencyKey: requestKey.current
|
|
5045
|
-
});
|
|
5046
|
-
if (result.creditAmount > 0) {
|
|
5047
|
-
toast.success(result.whole ? "Conta cancelada" : "Itens cancelados", {
|
|
5048
|
-
description: `${formatCurrency(result.creditAmount, currency)} em pagamentos ficaram como cr\xE9dito${counterpartyName ? ` de ${counterpartyName}` : ""}.`
|
|
5049
|
-
});
|
|
5050
|
-
} else {
|
|
5051
|
-
toast.success(result.whole ? "Conta cancelada" : "Itens cancelados");
|
|
5052
|
-
}
|
|
5053
|
-
onCancelled();
|
|
5054
|
-
} catch (err) {
|
|
5055
|
-
toast.error("N\xE3o foi poss\xEDvel cancelar", { description: err?.message });
|
|
5056
|
-
requestKey.current = createIdempotencyKey("financial:invoice-cancel-items");
|
|
5057
|
-
} finally {
|
|
5058
|
-
setBusy(false);
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
const loading = reasons === null || entries === null;
|
|
5062
|
-
return /* @__PURE__ */ jsxs(
|
|
5386
|
+
const total = entries.reduce((sum, e) => sum + e.quantity, 0);
|
|
5387
|
+
const stockCode = entries.find((e) => e.currency)?.currency;
|
|
5388
|
+
const stockCurrency = resolveCurrency(currency, stockCode);
|
|
5389
|
+
const stockValue = entries.reduce((sum, e) => sum + e.quantity * (e.unitCost ?? 0), 0);
|
|
5390
|
+
const paidCode = entries.find((e) => e.sourceCurrency)?.sourceCurrency;
|
|
5391
|
+
const paidValue = entries.reduce((sum, e) => sum + e.quantity * (e.sourceUnitCost ?? 0), 0);
|
|
5392
|
+
const rate = entries.find((e) => e.fxRate)?.fxRate;
|
|
5393
|
+
return /* @__PURE__ */ jsx(
|
|
5063
5394
|
ModalTask,
|
|
5064
5395
|
{
|
|
5065
5396
|
open: true,
|
|
5066
|
-
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2
|
|
5067
|
-
/* @__PURE__ */ jsx(
|
|
5068
|
-
"
|
|
5397
|
+
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
|
|
5398
|
+
/* @__PURE__ */ jsx(Package, { className: "h-4 w-4" }),
|
|
5399
|
+
" Entrada no estoque"
|
|
5069
5400
|
] }),
|
|
5070
5401
|
width: "max-w-lg",
|
|
5071
5402
|
onClose,
|
|
5072
|
-
children: [
|
|
5073
|
-
/* @__PURE__ */
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5403
|
+
children: /* @__PURE__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
|
|
5404
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: title }),
|
|
5405
|
+
/* @__PURE__ */ jsx("ul", { className: "divide-y rounded-lg border", children: entries.map((e, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 px-3 py-2.5 text-sm", children: [
|
|
5406
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
5407
|
+
/* @__PURE__ */ jsxs("p", { className: "truncate font-medium", children: [
|
|
5408
|
+
qty(e.quantity),
|
|
5409
|
+
real(e.variantName) ? ` \xD7 ${e.variantName}` : ""
|
|
5410
|
+
] }),
|
|
5411
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: [e.locationName, day2(e.enteredOn)].filter(Boolean).join(" \xB7 ") })
|
|
5078
5412
|
] }),
|
|
5079
|
-
|
|
5080
|
-
/* @__PURE__ */
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5413
|
+
/* @__PURE__ */ jsxs("div", { className: "shrink-0 text-right", children: [
|
|
5414
|
+
/* @__PURE__ */ jsxs("p", { className: "tabular-nums", children: [
|
|
5415
|
+
formatCurrency(e.unitCost ?? 0, resolveCurrency(currency, e.currency)),
|
|
5416
|
+
" cada"
|
|
5417
|
+
] }),
|
|
5418
|
+
e.sourceCurrency && e.sourceUnitCost != null && /* @__PURE__ */ jsxs("p", { className: "text-xs tabular-nums text-muted-foreground", children: [
|
|
5419
|
+
"pago ",
|
|
5420
|
+
formatCurrency(e.sourceUnitCost, resolveCurrency(currency, e.sourceCurrency))
|
|
5421
|
+
] })
|
|
5422
|
+
] })
|
|
5423
|
+
] }, i)) }),
|
|
5424
|
+
/* @__PURE__ */ jsxs("dl", { className: "space-y-1 text-sm", children: [
|
|
5425
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
5426
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Total recebido" }),
|
|
5427
|
+
/* @__PURE__ */ jsx("dd", { className: "font-medium tabular-nums", children: qty(total) })
|
|
5090
5428
|
] }),
|
|
5091
|
-
/* @__PURE__ */
|
|
5092
|
-
/* @__PURE__ */ jsx(
|
|
5093
|
-
/* @__PURE__ */ jsx("
|
|
5094
|
-
loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-16" }) : stocked.has(it.id) && /* @__PURE__ */ jsx(Badge, { variant: "success", className: "shrink-0", children: "No estoque" }),
|
|
5095
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(it.totalAmount, currency) })
|
|
5096
|
-
] }, it.id)) }),
|
|
5097
|
-
stockedSelected.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-4 rounded-lg border border-warning/40 bg-warning-soft p-3", children: [
|
|
5098
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-warning-soft-foreground", children: "Estes itens deram entrada no estoque. Diga o que acontece com a mercadoria: o destino vale para o saldo que ainda est\xE1 na posi\xE7\xE3o; o que j\xE1 foi consumido fica registrado." }),
|
|
5099
|
-
stockedSelected.map((it) => {
|
|
5100
|
-
const info = stocked.get(it.id);
|
|
5101
|
-
const choice = destinations[it.id];
|
|
5102
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
5103
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
5104
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: it.description }),
|
|
5105
|
-
/* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
5106
|
-
"Entrou no estoque: ",
|
|
5107
|
-
info.entered.toLocaleString("pt-BR"),
|
|
5108
|
-
" \xB7 ",
|
|
5109
|
-
day(info.enteredOn)
|
|
5110
|
-
] })
|
|
5111
|
-
] }),
|
|
5112
|
-
/* @__PURE__ */ jsx(
|
|
5113
|
-
SegmentedControl,
|
|
5114
|
-
{
|
|
5115
|
-
"aria-label": `Destino de ${it.description}`,
|
|
5116
|
-
options: DESTINATIONS,
|
|
5117
|
-
value: choice?.destination ?? "",
|
|
5118
|
-
onChange: (v) => setDestinations((d) => ({ ...d, [it.id]: { note: d[it.id]?.note ?? "", destination: v || void 0 } })),
|
|
5119
|
-
className: "flex-wrap"
|
|
5120
|
-
}
|
|
5121
|
-
),
|
|
5122
|
-
/* @__PURE__ */ jsx(
|
|
5123
|
-
"input",
|
|
5124
|
-
{
|
|
5125
|
-
value: choice?.note ?? "",
|
|
5126
|
-
onChange: (e) => setDestinations((d) => ({ ...d, [it.id]: { destination: d[it.id]?.destination, note: e.target.value } })),
|
|
5127
|
-
placeholder: "Observa\xE7\xE3o (ex.: nota de devolu\xE7\xE3o 8226400)",
|
|
5128
|
-
className: "h-9 w-full rounded-input border border-input bg-card px-3 text-sm"
|
|
5129
|
-
}
|
|
5130
|
-
)
|
|
5131
|
-
] }, it.id);
|
|
5132
|
-
})
|
|
5429
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
5430
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Valor em estoque" }),
|
|
5431
|
+
/* @__PURE__ */ jsx("dd", { className: "font-medium tabular-nums", children: formatCurrency(stockValue, stockCurrency) })
|
|
5133
5432
|
] }),
|
|
5134
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
5135
|
-
/* @__PURE__ */ jsx("
|
|
5136
|
-
/* @__PURE__ */ jsx("
|
|
5433
|
+
paidCode && /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
5434
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Valor pago" }),
|
|
5435
|
+
/* @__PURE__ */ jsx("dd", { className: "tabular-nums", children: formatCurrency(paidValue, resolveCurrency(currency, paidCode)) })
|
|
5137
5436
|
] }),
|
|
5138
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
5139
|
-
/* @__PURE__ */ jsx("
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
}
|
|
5149
|
-
),
|
|
5150
|
-
/* @__PURE__ */ jsx(
|
|
5151
|
-
"textarea",
|
|
5152
|
-
{
|
|
5153
|
-
value: detail,
|
|
5154
|
-
onChange: (e) => setDetail(e.target.value),
|
|
5155
|
-
placeholder: "Detalhe (opcional)...",
|
|
5156
|
-
className: "min-h-20 w-full resize-y rounded-input border border-input bg-card px-3 py-2 text-sm"
|
|
5157
|
-
}
|
|
5158
|
-
)
|
|
5159
|
-
] })
|
|
5160
|
-
] }),
|
|
5161
|
-
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3", children: [
|
|
5162
|
-
missing && !loading && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: missing }),
|
|
5163
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: "Voltar" }),
|
|
5164
|
-
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: submit, disabled: busy || loading || !!missing, children: [
|
|
5165
|
-
/* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
|
|
5166
|
-
busy ? "Cancelando\u2026" : whole ? "Cancelar conta" : "Cancelar itens"
|
|
5437
|
+
paidCode && rate && /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
5438
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "C\xE2mbio usado" }),
|
|
5439
|
+
/* @__PURE__ */ jsxs("dd", { className: "tabular-nums", children: [
|
|
5440
|
+
"1 ",
|
|
5441
|
+
paidCode,
|
|
5442
|
+
" = ",
|
|
5443
|
+
rate.toLocaleString("pt-BR", { maximumFractionDigits: 4 }),
|
|
5444
|
+
" ",
|
|
5445
|
+
stockCode
|
|
5446
|
+
] })
|
|
5167
5447
|
] })
|
|
5168
5448
|
] })
|
|
5169
|
-
]
|
|
5449
|
+
] })
|
|
5170
5450
|
}
|
|
5171
5451
|
);
|
|
5172
5452
|
}
|
|
5453
|
+
var ITEM_ICONS = {
|
|
5454
|
+
product: Package,
|
|
5455
|
+
service: Briefcase,
|
|
5456
|
+
gift_card: Gift,
|
|
5457
|
+
other: Receipt
|
|
5458
|
+
};
|
|
5173
5459
|
|
|
5174
5460
|
// src/views/focusItem.ts
|
|
5175
5461
|
var CHAVE = "fayz:financial:focus-item";
|
|
@@ -5194,9 +5480,9 @@ function consumirItemParaAbrir(invoiceId) {
|
|
|
5194
5480
|
}
|
|
5195
5481
|
}
|
|
5196
5482
|
function useStockedItemTypes(itemTypes, entityLookups) {
|
|
5197
|
-
const [empty, setEmpty] =
|
|
5483
|
+
const [empty, setEmpty] = React29__default.useState(null);
|
|
5198
5484
|
const backed = itemTypes.filter((type) => entityLookups[type.value]?.count).map((type) => type.value).sort().join(",");
|
|
5199
|
-
|
|
5485
|
+
React29__default.useEffect(() => {
|
|
5200
5486
|
const kinds = backed ? backed.split(",") : [];
|
|
5201
5487
|
if (!kinds.length) {
|
|
5202
5488
|
setEmpty(null);
|
|
@@ -5217,7 +5503,7 @@ function useStockedItemTypes(itemTypes, entityLookups) {
|
|
|
5217
5503
|
cancelled = true;
|
|
5218
5504
|
};
|
|
5219
5505
|
}, [backed]);
|
|
5220
|
-
return
|
|
5506
|
+
return React29__default.useMemo(
|
|
5221
5507
|
() => empty?.size ? itemTypes.filter((type) => !empty.has(type.value)) : itemTypes,
|
|
5222
5508
|
[itemTypes, empty]
|
|
5223
5509
|
);
|
|
@@ -5328,7 +5614,24 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5328
5614
|
const t2 = useTranslation();
|
|
5329
5615
|
const config = useFinancialConfig();
|
|
5330
5616
|
const provider = useFinancialProvider();
|
|
5331
|
-
const
|
|
5617
|
+
const [pictures, setPictures] = useState({});
|
|
5618
|
+
const asked = useRef(/* @__PURE__ */ new Set());
|
|
5619
|
+
const { itemTypes: itemTypesRaw = [], locations = [] } = config;
|
|
5620
|
+
const [documentCurrency, setDocumentCurrency] = useState();
|
|
5621
|
+
const [stockNotes, setStockNotes] = useState(/* @__PURE__ */ new Map());
|
|
5622
|
+
const currency = useCurrency(config.currency, documentCurrency);
|
|
5623
|
+
const houseCurrency = useCurrency(config.currency).code;
|
|
5624
|
+
const [currencyChoices, setCurrencyChoices] = useState([]);
|
|
5625
|
+
useEffect(() => {
|
|
5626
|
+
let alive = true;
|
|
5627
|
+
provider.listCurrencies?.().then((found) => {
|
|
5628
|
+
if (alive) setCurrencyChoices([.../* @__PURE__ */ new Set([houseCurrency, ...found])]);
|
|
5629
|
+
}).catch(() => {
|
|
5630
|
+
});
|
|
5631
|
+
return () => {
|
|
5632
|
+
alive = false;
|
|
5633
|
+
};
|
|
5634
|
+
}, [provider, houseCurrency]);
|
|
5332
5635
|
const { unitMode, activeUnitId, choices: unitChoices } = useWritableUnits(locations);
|
|
5333
5636
|
const reachableUnits = useUnitFilter().options;
|
|
5334
5637
|
const showUnitField = unitMode ? reachableUnits.length > 1 : unitChoices.length > 1;
|
|
@@ -5387,14 +5690,14 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5387
5690
|
};
|
|
5388
5691
|
}, [giftCardsEnabled, provider, t2]);
|
|
5389
5692
|
const entityLookups = useMemo(() => giftCardLookup ? { ...config.entityLookups, gift_card: giftCardLookup } : config.entityLookups, [config.entityLookups, giftCardLookup]);
|
|
5390
|
-
const translatedItemTypes =
|
|
5693
|
+
const translatedItemTypes = React29__default.useMemo(
|
|
5391
5694
|
() => itemTypesRaw.map((tp) => {
|
|
5392
5695
|
const traduzido = t2(`financial.itemType.${tp.value}`);
|
|
5393
5696
|
return traduzido === `financial.itemType.${tp.value}` ? tp : { ...tp, label: traduzido };
|
|
5394
5697
|
}),
|
|
5395
5698
|
[itemTypesRaw, t2]
|
|
5396
5699
|
);
|
|
5397
|
-
const offeredItemTypes =
|
|
5700
|
+
const offeredItemTypes = React29__default.useMemo(
|
|
5398
5701
|
() => availableInvoiceItemTypes(
|
|
5399
5702
|
giftCardsEnabled && !translatedItemTypes.some((type) => type.value === "gift_card") ? [...translatedItemTypes, { value: "gift_card", label: t2("financial.itemType.gift_card"), icon: "Gift" }] : translatedItemTypes,
|
|
5400
5703
|
Object.keys(entityLookups),
|
|
@@ -5418,7 +5721,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5418
5721
|
const isEdit = !!editId;
|
|
5419
5722
|
const [attachmentsOpen, setAttachmentsOpen] = useState(false);
|
|
5420
5723
|
const [attachmentCount, setAttachmentCount] = useState(0);
|
|
5421
|
-
const loadAttachmentCount =
|
|
5724
|
+
const loadAttachmentCount = React29__default.useCallback(() => {
|
|
5422
5725
|
if (!editId || !provider.getInvoiceAttachments) return;
|
|
5423
5726
|
provider.getInvoiceAttachments(editId).then((list) => setAttachmentCount(list.length)).catch(() => {
|
|
5424
5727
|
});
|
|
@@ -5473,6 +5776,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5473
5776
|
const [savedContactId, setSavedContactId] = useState();
|
|
5474
5777
|
const [fiscalNumber, setFiscalNumber] = useState("");
|
|
5475
5778
|
const [observations, setObservations] = useState("");
|
|
5779
|
+
const [tags, setTags] = useState([]);
|
|
5780
|
+
const [savedTags, setSavedTags] = useState("[]");
|
|
5476
5781
|
const [items, setItems] = useState([]);
|
|
5477
5782
|
const [accounts, setAccounts] = useState([]);
|
|
5478
5783
|
const postable = useMemo(() => postableAccounts(accounts), [accounts]);
|
|
@@ -5485,9 +5790,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5485
5790
|
const [expandedItemId, setExpandedItemId] = useState(null);
|
|
5486
5791
|
const [addingItem, setAddingItem] = useState(false);
|
|
5487
5792
|
const [installments, setInstallments] = useState([]);
|
|
5793
|
+
const [pinnedAmounts, setPinnedAmounts] = useState(/* @__PURE__ */ new Set());
|
|
5488
5794
|
const [cancelled, setCancelled] = useState(false);
|
|
5489
5795
|
const [savedTotal, setSavedTotal] = useState(null);
|
|
5490
5796
|
const [savedMovements, setSavedMovements] = useState([]);
|
|
5797
|
+
const [hasCancelledInstallments, setHasCancelledInstallments] = useState(false);
|
|
5491
5798
|
const [payingMovement, setPayingMovement] = useState(null);
|
|
5492
5799
|
const [pendingReceipt, setPendingReceipt] = useState();
|
|
5493
5800
|
const contactCreate = useLookupCreate(contactLookup, (created) => {
|
|
@@ -5560,8 +5867,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5560
5867
|
setSavedContactId(invoice.contactId);
|
|
5561
5868
|
setCancelled(invoice.status === "cancelled");
|
|
5562
5869
|
setSavedTotal(invoice.totalAmount);
|
|
5870
|
+
setDocumentCurrency(invoice.currency);
|
|
5563
5871
|
setFiscalNumber(String(invoice.metadata?.fiscalNumber ?? ""));
|
|
5564
5872
|
setObservations(invoice.observations ?? "");
|
|
5873
|
+
setTags(invoice.tags ?? []);
|
|
5874
|
+
setSavedTags(JSON.stringify(invoice.tags ?? []));
|
|
5565
5875
|
const allLines = await provider.getInvoiceItems(editId);
|
|
5566
5876
|
const invoiceItems = allLines.filter((it) => !it.cancelledAt);
|
|
5567
5877
|
setCancelledLines(allLines.filter((it) => !!it.cancelledAt));
|
|
@@ -5584,10 +5894,24 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5584
5894
|
metadata: it.metadata
|
|
5585
5895
|
};
|
|
5586
5896
|
}));
|
|
5897
|
+
void provider.getInvoiceStockDetails?.(editId).then((rows) => {
|
|
5898
|
+
const byDraft = /* @__PURE__ */ new Map();
|
|
5899
|
+
for (const r of rows) {
|
|
5900
|
+
const at = invoiceItems.findIndex((it) => it.id === r.itemId);
|
|
5901
|
+
const draftId = idsCarregados[at];
|
|
5902
|
+
if (!draftId) continue;
|
|
5903
|
+
byDraft.set(draftId, [...byDraft.get(draftId) ?? [], { variantName: r.variantName, quantity: r.quantity, enteredOn: r.enteredOn }]);
|
|
5904
|
+
}
|
|
5905
|
+
setStockNotes(byDraft);
|
|
5906
|
+
}).catch(() => {
|
|
5907
|
+
});
|
|
5587
5908
|
const aAbrir = consumirItemParaAbrir(editId);
|
|
5588
5909
|
if (aAbrir !== void 0 && idsCarregados[aAbrir]) setExpandedItemId(idsCarregados[aAbrir]);
|
|
5589
5910
|
const movements = await provider.getMovements({ invoiceId: editId });
|
|
5590
|
-
const
|
|
5911
|
+
const billMovements = movements.data.filter((m) => m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0));
|
|
5912
|
+
const liveMovements = billMovements.filter((m) => m.status !== "cancelled");
|
|
5913
|
+
const invoiceMovements = liveMovements.length > 0 ? liveMovements : billMovements;
|
|
5914
|
+
setHasCancelledInstallments(liveMovements.length > 0 && liveMovements.length < billMovements.length);
|
|
5591
5915
|
setSavedMovements(invoiceMovements);
|
|
5592
5916
|
setInstallments(invoiceMovements.map((m) => {
|
|
5593
5917
|
const meta = m.metadata ?? {};
|
|
@@ -5631,11 +5955,19 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5631
5955
|
const count = installments.length;
|
|
5632
5956
|
const perInstallment = Math.floor(scheduleTotal * 100 / count) / 100;
|
|
5633
5957
|
const remainder = Math.round((scheduleTotal - perInstallment * count) * 100) / 100;
|
|
5958
|
+
setPinnedAmounts(/* @__PURE__ */ new Set());
|
|
5634
5959
|
setInstallments((prev) => prev.map((inst, i) => ({
|
|
5635
5960
|
...inst,
|
|
5636
5961
|
amount: i === 0 ? perInstallment + remainder : perInstallment
|
|
5637
5962
|
})));
|
|
5638
5963
|
}, [scheduleTotal, somaParcelas, installments.length, isEdit]);
|
|
5964
|
+
useEffect(() => {
|
|
5965
|
+
const ids = items.filter((it) => it.itemKind === "product" && it.referenceId && !asked.current.has(it.referenceId)).map((it) => it.referenceId);
|
|
5966
|
+
if (ids.length === 0 || !provider.getProductPictures) return;
|
|
5967
|
+
ids.forEach((id) => asked.current.add(id));
|
|
5968
|
+
provider.getProductPictures(ids).then((found) => setPictures((prev) => ({ ...prev, ...found }))).catch(() => {
|
|
5969
|
+
});
|
|
5970
|
+
}, [items, provider]);
|
|
5639
5971
|
function addItemOfKind(kind) {
|
|
5640
5972
|
const newItem = {
|
|
5641
5973
|
_id: fid(),
|
|
@@ -5701,13 +6033,13 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5701
6033
|
if (isEdit || totalAmount <= 0 || installments.length > 0) return;
|
|
5702
6034
|
generateInstallments(1);
|
|
5703
6035
|
}, [isEdit, totalAmount, installments.length]);
|
|
5704
|
-
const fromCard =
|
|
6036
|
+
const fromCard = React29__default.useMemo(
|
|
5705
6037
|
() => fromCardId ? bankAccounts.find((a) => a.id === fromCardId) : void 0,
|
|
5706
6038
|
[fromCardId, bankAccounts]
|
|
5707
6039
|
);
|
|
5708
6040
|
const cardLast4 = (fromCard?.accountNumber ?? "").replace(/\D/g, "").slice(-4);
|
|
5709
6041
|
const nomeJaTemOsDigitos = !!cardLast4 && (fromCard?.name ?? "").replace(/\D/g, "").endsWith(cardLast4);
|
|
5710
|
-
const cardMethodName =
|
|
6042
|
+
const cardMethodName = React29__default.useMemo(() => {
|
|
5711
6043
|
const id = installments[0]?.payMethodTypeId;
|
|
5712
6044
|
const escolhida = id ? paymentMethodTypes.find((tp) => tp.id === id) : void 0;
|
|
5713
6045
|
return (escolhida ?? paymentMethodTypes.find((tp) => tp.transactionType === "credit_card"))?.name;
|
|
@@ -5843,6 +6175,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5843
6175
|
unitId: unitId || void 0,
|
|
5844
6176
|
fiscalNumber: fiscalNumber || void 0,
|
|
5845
6177
|
observations,
|
|
6178
|
+
// Only when changed: an untouched bill must not write a column an older pool lacks.
|
|
6179
|
+
...JSON.stringify(tags) !== savedTags ? { tags } : {},
|
|
5846
6180
|
// 098 abriu a competência enquanto nada foi recebido.
|
|
5847
6181
|
...invoiceDate !== savedInvoiceDate ? { invoiceDate } : {},
|
|
5848
6182
|
// 110 abriu a CONTRAPARTE, com o mesmo limite: quem digita seis
|
|
@@ -5882,6 +6216,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5882
6216
|
contactId,
|
|
5883
6217
|
contactName: contactName || void 0,
|
|
5884
6218
|
unitId: unitId || void 0,
|
|
6219
|
+
currency: documentCurrency,
|
|
6220
|
+
tags,
|
|
5885
6221
|
fiscalNumber: fiscalNumber || void 0,
|
|
5886
6222
|
observations: observations || void 0,
|
|
5887
6223
|
items: items.map(({ _id, ...rest }) => rest),
|
|
@@ -5958,6 +6294,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
5958
6294
|
invoiceDate,
|
|
5959
6295
|
fiscalNumber,
|
|
5960
6296
|
observations,
|
|
6297
|
+
tags,
|
|
5961
6298
|
items: items.map(({ _id, ...rest }) => rest),
|
|
5962
6299
|
installments: installments.map(({ _id, ...rest }) => rest)
|
|
5963
6300
|
};
|
|
@@ -6009,12 +6346,12 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6009
6346
|
] }) : null;
|
|
6010
6347
|
const paidSoFar = savedMovements.reduce((sum, m) => sum + (m.paidAmount ?? 0), 0);
|
|
6011
6348
|
const hasPayments = paidSoFar > 0;
|
|
6012
|
-
const canReschedule = !hasPayments && (!isEdit || !!provider.rescheduleInvoice);
|
|
6013
|
-
const canEditItems = !hasPayments && (!isEdit || !!provider.replaceInvoiceItems);
|
|
6349
|
+
const canReschedule = !hasPayments && !hasCancelledInstallments && (!isEdit || !!provider.rescheduleInvoice);
|
|
6350
|
+
const canEditItems = !hasPayments && !hasCancelledInstallments && (!isEdit || !!provider.replaceInvoiceItems);
|
|
6014
6351
|
const scheduleSection = (
|
|
6015
6352
|
// Tour anchor: the guided setup points here for the installment schedule.
|
|
6016
6353
|
/* @__PURE__ */ jsx("div", { ...tourAnchor("financial.installments"), children: /* @__PURE__ */ jsx(OrderFormBlock, { title: t2("financial.invoice.paymentSchedule"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
6017
|
-
hasPayments ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoiceForm.scheduleLocked") }) : !canReschedule ? null : (
|
|
6354
|
+
hasPayments ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoiceForm.scheduleLocked") }) : hasCancelledInstallments ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoiceForm.scheduleHasCancelled") }) : !canReschedule ? null : (
|
|
6018
6355
|
// Atalhos de parcelamento — 1x, 2x, 3x, 6x, 12x cobrem quase tudo.
|
|
6019
6356
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: [1, 2, 3, 6, 12].map((n) => /* @__PURE__ */ jsxs(
|
|
6020
6357
|
"button",
|
|
@@ -6041,6 +6378,35 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6041
6378
|
const saved = savedMovements[i];
|
|
6042
6379
|
const isPaid2 = saved?.status === "paid";
|
|
6043
6380
|
const isPartial = saved?.status === "partial";
|
|
6381
|
+
const lockedAmounts = installments.map((_, n) => {
|
|
6382
|
+
const st = savedMovements[n]?.status;
|
|
6383
|
+
return st === "paid" || st === "partial" || st === "cancelled";
|
|
6384
|
+
});
|
|
6385
|
+
const changeAmount = (value) => {
|
|
6386
|
+
const next = rebalanceInstallments({
|
|
6387
|
+
amounts: installments.map((x) => x.amount),
|
|
6388
|
+
index: i,
|
|
6389
|
+
value,
|
|
6390
|
+
total: scheduleTotal,
|
|
6391
|
+
locked: lockedAmounts,
|
|
6392
|
+
pinned: installments.map((x) => pinnedAmounts.has(x._id))
|
|
6393
|
+
});
|
|
6394
|
+
if (next) {
|
|
6395
|
+
setPinnedAmounts((prev) => new Set(prev).add(inst._id));
|
|
6396
|
+
setInstallments(installments.map((x, n) => ({ ...x, amount: next[n] })));
|
|
6397
|
+
return;
|
|
6398
|
+
}
|
|
6399
|
+
const rest = Math.round((inst.amount - value) * 100) / 100;
|
|
6400
|
+
if (installments.length !== 1 || value <= 0 || rest <= 0) return;
|
|
6401
|
+
const due = /* @__PURE__ */ new Date(`${inst.dueDate}T12:00:00`);
|
|
6402
|
+
due.setMonth(due.getMonth() + 1);
|
|
6403
|
+
setPinnedAmounts((prev) => new Set(prev).add(inst._id));
|
|
6404
|
+
setInstallments([
|
|
6405
|
+
{ ...inst, amount: value },
|
|
6406
|
+
{ _id: fid(), amount: rest, dueDate: due.toISOString().slice(0, 10) }
|
|
6407
|
+
]);
|
|
6408
|
+
};
|
|
6409
|
+
const canChangeAmount = canReschedule && !lockedAmounts[i] && (installments.length === 1 || installments.some((_, n) => n !== i && !lockedAmounts[n]));
|
|
6044
6410
|
const canAttach = isEdit ? !!saved && !isPaid2 : true;
|
|
6045
6411
|
const semFonte = Math.abs(inst.amount) < 5e-3 && !isPaid2 && !isPartial;
|
|
6046
6412
|
const fonteTravada = isPaid2 || isPartial;
|
|
@@ -6053,6 +6419,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6053
6419
|
{
|
|
6054
6420
|
number: i + 1,
|
|
6055
6421
|
amount: money4(inst.amount),
|
|
6422
|
+
amountValue: inst.amount,
|
|
6423
|
+
onAmountChange: canChangeAmount ? changeAmount : void 0,
|
|
6056
6424
|
status: saved?.status,
|
|
6057
6425
|
dueDate: inst.dueDate,
|
|
6058
6426
|
onDueDateChange: !hasPayments ? (v) => patch({ dueDate: v }) : void 0,
|
|
@@ -6240,7 +6608,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6240
6608
|
rows: 2,
|
|
6241
6609
|
className: "w-full resize-none rounded-input border border-input bg-card px-3 py-2 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
|
|
6242
6610
|
}
|
|
6243
|
-
) })
|
|
6611
|
+
) }),
|
|
6612
|
+
/* @__PURE__ */ jsx(OrderFormBlock, { title: t2("financial.invoiceForm.tags"), children: /* @__PURE__ */ jsx(TagsField, { value: tags, onChange: setTags, table: "plg_financial_invoices" }) })
|
|
6244
6613
|
] });
|
|
6245
6614
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6246
6615
|
!isEdit && direction === "debit" && /* @__PURE__ */ jsx(
|
|
@@ -6307,6 +6676,16 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6307
6676
|
"data-tour": "financial.items",
|
|
6308
6677
|
label: t2("financial.invoiceForm.items"),
|
|
6309
6678
|
addLabel: t2("financial.invoiceForm.addItem"),
|
|
6679
|
+
headerExtra: !isEdit && currencyChoices.length > 1 ? /* @__PURE__ */ jsx(
|
|
6680
|
+
SegmentedControl,
|
|
6681
|
+
{
|
|
6682
|
+
size: "md",
|
|
6683
|
+
"aria-label": t2("financial.invoiceForm.currency"),
|
|
6684
|
+
options: currencyChoices,
|
|
6685
|
+
value: currency.code,
|
|
6686
|
+
onChange: setDocumentCurrency
|
|
6687
|
+
}
|
|
6688
|
+
) : void 0,
|
|
6310
6689
|
onAdd: canEditItems ? startAddItem : void 0,
|
|
6311
6690
|
addDisabled: !contactId,
|
|
6312
6691
|
addDisabledHint: t2("financial.invoiceForm.selectContactFirst"),
|
|
@@ -6338,6 +6717,9 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6338
6717
|
discount: item.discount,
|
|
6339
6718
|
surcharge: item.surcharge,
|
|
6340
6719
|
referenceId: item.referenceId,
|
|
6720
|
+
icon: ITEM_ICONS[item.itemKind] ?? ITEM_ICONS.other,
|
|
6721
|
+
imageUrl: item.itemKind === "product" && item.referenceId ? pictures[item.referenceId] : void 0,
|
|
6722
|
+
note: stockNotes.has(item._id) ? /* @__PURE__ */ jsx(StockEntryNote, { entries: stockNotes.get(item._id) }) : void 0,
|
|
6341
6723
|
itemTypes,
|
|
6342
6724
|
currency,
|
|
6343
6725
|
formatMoney: money4,
|
|
@@ -6457,7 +6839,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6457
6839
|
),
|
|
6458
6840
|
receiptPicker.input,
|
|
6459
6841
|
attachmentsOpen && editId && /* @__PURE__ */ jsx(InvoiceAttachmentsModal, { invoiceId: editId, onClose: () => setAttachmentsOpen(false), onChanged: loadAttachmentCount }),
|
|
6460
|
-
payingMovement && /* @__PURE__ */ jsx(
|
|
6842
|
+
payingMovement && /* @__PURE__ */ jsx(CurrencyScope, { code: documentCurrency, children: /* @__PURE__ */ jsx(
|
|
6461
6843
|
PaymentModal,
|
|
6462
6844
|
{
|
|
6463
6845
|
movement: payingMovement,
|
|
@@ -6490,7 +6872,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
6490
6872
|
})();
|
|
6491
6873
|
}
|
|
6492
6874
|
}
|
|
6493
|
-
),
|
|
6875
|
+
) }),
|
|
6494
6876
|
cancelData && editId && /* @__PURE__ */ jsx(
|
|
6495
6877
|
CancelInvoiceDialog,
|
|
6496
6878
|
{
|
|
@@ -6587,7 +6969,13 @@ function PaymentReceiptModal({
|
|
|
6587
6969
|
const fmt = (n) => formatCurrency(n, currency);
|
|
6588
6970
|
const ordenados = [...payments].sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? "") || (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
|
|
6589
6971
|
const estornaveis = new Set(reversiblePayments(ordenados).map((m) => m.id));
|
|
6590
|
-
const
|
|
6972
|
+
const vivos = ordenados.filter((m) => estornaveis.has(m.id));
|
|
6973
|
+
const desfeitos = ordenados.filter((m) => !ehEstorno(m) && !estornaveis.has(m.id)).map((m) => ({
|
|
6974
|
+
payment: m,
|
|
6975
|
+
reversal: ordenados.find((r) => r.metadata?.reversal_of === m.id)
|
|
6976
|
+
}));
|
|
6977
|
+
const [verDesfeitos, setVerDesfeitos] = React29__default.useState(false);
|
|
6978
|
+
const total = vivos.reduce((soma, m) => soma + (m.paidAmount || m.amount), 0);
|
|
6591
6979
|
return /* @__PURE__ */ jsx(Modal, { open: true, onOpenChange: (o) => {
|
|
6592
6980
|
if (!o) onClose();
|
|
6593
6981
|
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
|
|
@@ -6596,15 +6984,15 @@ function PaymentReceiptModal({
|
|
|
6596
6984
|
t2("financial.paymentReceipt.title")
|
|
6597
6985
|
] }) }),
|
|
6598
6986
|
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-2", children: [
|
|
6599
|
-
|
|
6600
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t2("financial.paymentReceipt.count", { count:
|
|
6987
|
+
vivos.length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-card bg-muted/60 px-4 py-2", children: [
|
|
6988
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t2("financial.paymentReceipt.count", { count: vivos.length }) }),
|
|
6601
6989
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-success", children: fmt(total) })
|
|
6602
6990
|
] }),
|
|
6603
|
-
|
|
6991
|
+
vivos.map((payment, indice) => {
|
|
6604
6992
|
const how = describePayment(payment, howCtx);
|
|
6605
6993
|
const estorno = ehEstorno(payment);
|
|
6606
6994
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2.5 rounded-card bg-muted/40 px-4 py-3", children: [
|
|
6607
|
-
|
|
6995
|
+
vivos.length > 1 && /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: estorno ? t2("financial.paymentReceipt.reversalNumber", { number: indice + 1 }) : t2("financial.paymentReceipt.paymentNumber", { number: indice + 1 }) }),
|
|
6608
6996
|
payment.paymentDate && /* @__PURE__ */ jsx(Campo, { icon: Calendar, label: t2("financial.paymentReceipt.date"), children: dataBR(payment.paymentDate) }),
|
|
6609
6997
|
/* @__PURE__ */ jsxs(Campo, { icon: DollarSign, label: t2("financial.paymentReceipt.amount"), tone: "money", children: [
|
|
6610
6998
|
estorno ? "\u2212 " : "",
|
|
@@ -6647,17 +7035,268 @@ function PaymentReceiptModal({
|
|
|
6647
7035
|
] })
|
|
6648
7036
|
] }, payment.id);
|
|
6649
7037
|
}),
|
|
6650
|
-
|
|
7038
|
+
vivos.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: t2("financial.paymentReceipt.empty") }),
|
|
7039
|
+
desfeitos.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-dashed px-4 py-2", children: [
|
|
7040
|
+
/* @__PURE__ */ jsxs(
|
|
7041
|
+
"button",
|
|
7042
|
+
{
|
|
7043
|
+
type: "button",
|
|
7044
|
+
className: "flex w-full items-center justify-between text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
7045
|
+
onClick: () => setVerDesfeitos((v) => !v),
|
|
7046
|
+
children: [
|
|
7047
|
+
/* @__PURE__ */ jsx("span", { children: t2("financial.paymentReceipt.reversedCount", { count: desfeitos.length }) }),
|
|
7048
|
+
/* @__PURE__ */ jsx("span", { children: verDesfeitos ? t2("financial.paymentReceipt.hide") : t2("financial.paymentReceipt.show") })
|
|
7049
|
+
]
|
|
7050
|
+
}
|
|
7051
|
+
),
|
|
7052
|
+
verDesfeitos && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-2", children: desfeitos.map(({ payment, reversal }) => {
|
|
7053
|
+
const how = describePayment(payment, howCtx);
|
|
7054
|
+
const reason = reversal?.metadata?.reason;
|
|
7055
|
+
return /* @__PURE__ */ jsxs("li", { className: "text-xs text-muted-foreground", children: [
|
|
7056
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
7057
|
+
/* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
7058
|
+
payment.paymentDate ? dataBR(payment.paymentDate) : "",
|
|
7059
|
+
how.account ? ` \xB7 ${how.account.name}` : ""
|
|
7060
|
+
] }),
|
|
7061
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 line-through", children: fmt(payment.paidAmount || payment.amount) })
|
|
7062
|
+
] }),
|
|
7063
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
7064
|
+
reversal?.paymentDate ? t2("financial.paymentReceipt.reversedOn", { date: dataBR(reversal.paymentDate) }) : t2("financial.paymentReceipt.reversed"),
|
|
7065
|
+
reason ? ` \u2014 ${reason}` : ""
|
|
7066
|
+
] })
|
|
7067
|
+
] }, payment.id);
|
|
7068
|
+
}) })
|
|
7069
|
+
] })
|
|
6651
7070
|
] }),
|
|
6652
7071
|
/* @__PURE__ */ jsx(ModalFooter, { children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: t2("common.close") }) })
|
|
6653
7072
|
] }) });
|
|
6654
7073
|
}
|
|
6655
|
-
var
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
7074
|
+
var num2 = (text2) => Number(text2.replace(",", ".")) || 0;
|
|
7075
|
+
function ReceiveStockDialog({ item, invoiceCurrency, entered, onClose, onReceived }) {
|
|
7076
|
+
const { currency } = useFinancialConfig();
|
|
7077
|
+
const provider = useFinancialProvider();
|
|
7078
|
+
const house = useHouseCurrency();
|
|
7079
|
+
const billCurrency = resolveCurrency(currency, invoiceCurrency);
|
|
7080
|
+
const linkedProductId = item.itemKind === "product" ? item.referenceId : void 0;
|
|
7081
|
+
const [locations, setLocations] = useState(null);
|
|
7082
|
+
const [locationId, setLocationId] = useState();
|
|
7083
|
+
const [product, setProduct] = useState(null);
|
|
7084
|
+
const [variants, setVariants] = useState(null);
|
|
7085
|
+
const [quantities, setQuantities] = useState({});
|
|
7086
|
+
const billedUnitCost = item.quantity ? item.totalAmount / item.quantity : item.unitPrice;
|
|
7087
|
+
const [unitCost, setUnitCost] = useState(Math.round(billedUnitCost * 100) / 100);
|
|
7088
|
+
const [costTouched, setCostTouched] = useState(false);
|
|
7089
|
+
const [fx, setFx] = useState(void 0);
|
|
7090
|
+
const [busy, setBusy] = useState(false);
|
|
7091
|
+
const requestKey = useRef(createIdempotencyKey("financial:invoice-item-stock"));
|
|
7092
|
+
const alreadyIn = (entered ?? []).reduce((sum, e) => sum + e.quantity, 0);
|
|
7093
|
+
const productId = linkedProductId ?? product?.id;
|
|
7094
|
+
useEffect(() => {
|
|
7095
|
+
let alive = true;
|
|
7096
|
+
(provider.listStockLocations?.() ?? Promise.resolve([])).then((rows) => {
|
|
7097
|
+
if (!alive) return;
|
|
7098
|
+
setLocations(rows);
|
|
7099
|
+
if (rows.length === 1) setLocationId(rows[0].id);
|
|
7100
|
+
});
|
|
7101
|
+
if (!linkedProductId) {
|
|
7102
|
+
void provider.searchStockProducts?.(item.description.slice(0, 24)).then((rows) => {
|
|
7103
|
+
if (alive && rows.length === 1) setProduct(rows[0]);
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
return () => {
|
|
7107
|
+
alive = false;
|
|
7108
|
+
};
|
|
7109
|
+
}, [provider, item.id]);
|
|
7110
|
+
useEffect(() => {
|
|
7111
|
+
if (!productId) {
|
|
7112
|
+
setVariants(null);
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
7115
|
+
let alive = true;
|
|
7116
|
+
setVariants(null);
|
|
7117
|
+
(provider.listProductVariants?.(productId) ?? Promise.resolve([])).then((rows) => {
|
|
7118
|
+
if (!alive) return;
|
|
7119
|
+
const sized = rows.filter((v) => !v.isDefault);
|
|
7120
|
+
const shown = sized.length > 0 ? sized : rows.slice(0, 1);
|
|
7121
|
+
setVariants(shown);
|
|
7122
|
+
const remaining = Math.max(item.quantity - alreadyIn, 0);
|
|
7123
|
+
setQuantities(shown.length === 1 ? { [shown[0].id]: String(remaining || item.quantity) } : {});
|
|
7124
|
+
});
|
|
7125
|
+
return () => {
|
|
7126
|
+
alive = false;
|
|
7127
|
+
};
|
|
7128
|
+
}, [provider, productId]);
|
|
7129
|
+
const single = (variants?.length ?? 0) <= 1;
|
|
7130
|
+
const entries = useMemo(
|
|
7131
|
+
() => (variants ?? []).map((v) => ({ variant: v, quantity: num2(quantities[v.id] ?? "") })).filter((e) => e.quantity > 0),
|
|
7132
|
+
[variants, quantities]
|
|
7133
|
+
);
|
|
7134
|
+
const total = entries.reduce((sum, e) => sum + e.quantity, 0);
|
|
7135
|
+
const foreign = !!invoiceCurrency && !!house && invoiceCurrency !== house;
|
|
7136
|
+
useEffect(() => {
|
|
7137
|
+
if (costTouched || total <= 0) return;
|
|
7138
|
+
setUnitCost(Math.round(item.totalAmount / total * 100) / 100);
|
|
7139
|
+
}, [costTouched, total, item.totalAmount]);
|
|
7140
|
+
useEffect(() => {
|
|
7141
|
+
if (!foreign || !invoiceCurrency || !house) return;
|
|
7142
|
+
let alive = true;
|
|
7143
|
+
setFx(void 0);
|
|
7144
|
+
(provider.getFxRate?.(invoiceCurrency, house) ?? Promise.resolve(null)).then((value) => {
|
|
7145
|
+
if (alive) setFx(value);
|
|
7146
|
+
}).catch(() => {
|
|
7147
|
+
if (alive) setFx(null);
|
|
7148
|
+
});
|
|
7149
|
+
return () => {
|
|
7150
|
+
alive = false;
|
|
7151
|
+
};
|
|
7152
|
+
}, [provider, foreign, invoiceCurrency, house]);
|
|
7153
|
+
const missing = !productId ? "Escolha o produto desta linha" : !locationId ? "Escolha onde a mercadoria entra" : total <= 0 ? "Informe quanto chegou" : null;
|
|
7154
|
+
async function submit() {
|
|
7155
|
+
if (missing || !locationId || !provider.receiveInvoiceItemStock) return;
|
|
7156
|
+
setBusy(true);
|
|
7157
|
+
try {
|
|
7158
|
+
await provider.receiveInvoiceItemStock({
|
|
7159
|
+
itemId: item.id,
|
|
7160
|
+
locationId,
|
|
7161
|
+
productId: linkedProductId ? void 0 : product?.id,
|
|
7162
|
+
// The stand-in variant is the stock side's default; naming it is not needed.
|
|
7163
|
+
entries: entries.map((e) => ({ variantId: e.variant.isDefault ? void 0 : e.variant.id, quantity: e.quantity })),
|
|
7164
|
+
unitCost,
|
|
7165
|
+
fxRate: foreign && fx ? fx : void 0,
|
|
7166
|
+
idempotencyKey: requestKey.current
|
|
7167
|
+
});
|
|
7168
|
+
toast.success(`${total.toLocaleString("pt-BR")} em estoque`);
|
|
7169
|
+
onReceived();
|
|
7170
|
+
} catch (err) {
|
|
7171
|
+
toast.error("N\xE3o foi poss\xEDvel dar entrada", { description: err?.message });
|
|
7172
|
+
requestKey.current = createIdempotencyKey("financial:invoice-item-stock");
|
|
7173
|
+
} finally {
|
|
7174
|
+
setBusy(false);
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
return /* @__PURE__ */ jsxs(
|
|
7178
|
+
ModalTask,
|
|
7179
|
+
{
|
|
7180
|
+
open: true,
|
|
7181
|
+
title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
|
|
7182
|
+
/* @__PURE__ */ jsx(PackagePlus, { className: "h-4 w-4" }),
|
|
7183
|
+
" Dar entrada no estoque"
|
|
7184
|
+
] }),
|
|
7185
|
+
width: "max-w-lg",
|
|
7186
|
+
onClose,
|
|
7187
|
+
children: [
|
|
7188
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
|
|
7189
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border px-3 py-2.5", children: [
|
|
7190
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
7191
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: item.description }),
|
|
7192
|
+
/* @__PURE__ */ jsxs("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: [
|
|
7193
|
+
item.quantity,
|
|
7194
|
+
" \xD7 ",
|
|
7195
|
+
formatCurrency(billedUnitCost, billCurrency)
|
|
7196
|
+
] })
|
|
7197
|
+
] }),
|
|
7198
|
+
alreadyIn > 0 && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
7199
|
+
"J\xE1 entrou: ",
|
|
7200
|
+
alreadyIn.toLocaleString("pt-BR"),
|
|
7201
|
+
(entered ?? []).some((e) => e.variantName) ? ` (${(entered ?? []).map((e) => `${e.quantity.toLocaleString("pt-BR")} ${e.variantName ?? ""}`.trim()).join(", ")})` : ""
|
|
7202
|
+
] })
|
|
7203
|
+
] }),
|
|
7204
|
+
!linkedProductId && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
7205
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Produto" }),
|
|
7206
|
+
/* @__PURE__ */ jsx(
|
|
7207
|
+
SearchSelect,
|
|
7208
|
+
{
|
|
7209
|
+
value: product?.id,
|
|
7210
|
+
displayValue: product?.name,
|
|
7211
|
+
onChange: (id, option) => setProduct(id && option ? { id, name: option.label } : null),
|
|
7212
|
+
onSearch: async (q) => (await provider.searchStockProducts?.(q) ?? []).map((r) => ({ id: r.id, label: r.name })),
|
|
7213
|
+
minChars: 0,
|
|
7214
|
+
placeholder: "Buscar produto do cat\xE1logo..."
|
|
7215
|
+
}
|
|
7216
|
+
),
|
|
7217
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "A linha passa a apontar para este produto." })
|
|
7218
|
+
] }),
|
|
7219
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
7220
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Local" }),
|
|
7221
|
+
locations === null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }) : locations.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Nenhum local de estoque cadastrado." }) : /* @__PURE__ */ jsx(
|
|
7222
|
+
ChoiceSelect,
|
|
7223
|
+
{
|
|
7224
|
+
"aria-label": "Local de estoque",
|
|
7225
|
+
options: locations.map((l) => ({ value: l.id, label: l.name })),
|
|
7226
|
+
value: locationId,
|
|
7227
|
+
onChange: setLocationId,
|
|
7228
|
+
placeholder: "Selecione o local..."
|
|
7229
|
+
}
|
|
7230
|
+
)
|
|
7231
|
+
] }),
|
|
7232
|
+
productId && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
7233
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between", children: [
|
|
7234
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: single ? "Quantidade recebida" : "Quanto chegou de cada varia\xE7\xE3o" }),
|
|
7235
|
+
!single && total > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs tabular-nums text-muted-foreground", children: [
|
|
7236
|
+
total.toLocaleString("pt-BR"),
|
|
7237
|
+
" no total \xB7 ",
|
|
7238
|
+
item.quantity.toLocaleString("pt-BR"),
|
|
7239
|
+
" na conta"
|
|
7240
|
+
] })
|
|
7241
|
+
] }),
|
|
7242
|
+
variants === null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }) : /* @__PURE__ */ jsx("div", { className: single ? "" : "grid grid-cols-2 gap-2 @md:grid-cols-3", children: variants.map((v) => /* @__PURE__ */ jsxs("label", { className: single ? "block" : "flex items-center gap-2 rounded-lg border px-3 py-2", children: [
|
|
7243
|
+
!single && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: v.name }),
|
|
7244
|
+
/* @__PURE__ */ jsx(
|
|
7245
|
+
"input",
|
|
7246
|
+
{
|
|
7247
|
+
inputMode: "decimal",
|
|
7248
|
+
"aria-label": single ? "Quantidade recebida" : `Quantidade de ${v.name}`,
|
|
7249
|
+
value: quantities[v.id] ?? "",
|
|
7250
|
+
onChange: (e) => setQuantities((q) => ({ ...q, [v.id]: e.target.value })),
|
|
7251
|
+
placeholder: "0",
|
|
7252
|
+
className: `h-9 rounded-input border border-input bg-card px-3 text-sm tabular-nums ${single ? "w-full" : "w-16 text-right"}`
|
|
7253
|
+
}
|
|
7254
|
+
)
|
|
7255
|
+
] }, v.id)) }),
|
|
7256
|
+
single && variants !== null && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Este produto n\xE3o tem varia\xE7\xF5es. Para separar por tamanho ou cor, cadastre-as no produto (aba Varia\xE7\xF5es)." })
|
|
7257
|
+
] }),
|
|
7258
|
+
productId && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
7259
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Custo por unidade" }),
|
|
7260
|
+
/* @__PURE__ */ jsx(
|
|
7261
|
+
CurrencyInput,
|
|
7262
|
+
{
|
|
7263
|
+
value: unitCost,
|
|
7264
|
+
onChange: (v) => {
|
|
7265
|
+
setCostTouched(true);
|
|
7266
|
+
setUnitCost(v);
|
|
7267
|
+
},
|
|
7268
|
+
symbol: billCurrency.symbol,
|
|
7269
|
+
locale: billCurrency.locale,
|
|
7270
|
+
currencyCode: billCurrency.code
|
|
7271
|
+
}
|
|
7272
|
+
),
|
|
7273
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
7274
|
+
costTouched ? /* @__PURE__ */ jsx(
|
|
7275
|
+
"button",
|
|
7276
|
+
{
|
|
7277
|
+
type: "button",
|
|
7278
|
+
onClick: () => setCostTouched(false),
|
|
7279
|
+
className: "text-primary underline decoration-dashed underline-offset-2 hover:opacity-80",
|
|
7280
|
+
children: "Voltar ao custo calculado"
|
|
7281
|
+
}
|
|
7282
|
+
) : total > 0 ? `${formatCurrency(item.totalAmount, billCurrency)} da linha \xF7 ${total.toLocaleString("pt-BR")} recebidas.` : "Calculado pelo valor da linha e pela quantidade recebida.",
|
|
7283
|
+
foreign && fx ? ` Em estoque: ${formatCurrency(unitCost * fx, resolveCurrency(currency, house))} cada.` : ""
|
|
7284
|
+
] })
|
|
7285
|
+
] }),
|
|
7286
|
+
foreign && productId && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: fx === void 0 ? "Buscando o c\xE2mbio do dia\u2026" : fx ? `C\xE2mbio do dia: 1 ${invoiceCurrency} = ${fx.toLocaleString("pt-BR", { maximumFractionDigits: 4 })} ${house}. A conta \xE9 em ${invoiceCurrency} e o estoque \xE9 guardado em ${house}.` : `Sem c\xE2mbio ${invoiceCurrency} \u2192 ${house} dispon\xEDvel agora. A entrada tenta buscar de novo ao confirmar.` })
|
|
7287
|
+
] }),
|
|
7288
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3", children: [
|
|
7289
|
+
missing && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: missing }),
|
|
7290
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: "Voltar" }),
|
|
7291
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: submit, disabled: busy || !!missing, children: [
|
|
7292
|
+
/* @__PURE__ */ jsx(PackagePlus, { className: "h-3.5 w-3.5" }),
|
|
7293
|
+
busy ? "Lan\xE7ando\u2026" : total > 0 ? `Dar entrada em ${total.toLocaleString("pt-BR")}` : "Dar entrada"
|
|
7294
|
+
] })
|
|
7295
|
+
] })
|
|
7296
|
+
]
|
|
7297
|
+
}
|
|
7298
|
+
);
|
|
7299
|
+
}
|
|
6661
7300
|
var STATUS_COLORS = {
|
|
6662
7301
|
open: { bg: "bg-info-soft text-info-soft-foreground", icon: CircleDashed, labelKey: "financial.invoice.statusOpen", tone: "info" },
|
|
6663
7302
|
pending: { bg: "bg-warning-soft text-warning-soft-foreground", icon: CircleDashed, labelKey: "financial.invoice.statusPending", tone: "warning" },
|
|
@@ -6675,7 +7314,6 @@ function itemKindLabel(kind, t2) {
|
|
|
6675
7314
|
function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, embedded }) {
|
|
6676
7315
|
const t2 = useTranslation();
|
|
6677
7316
|
const config = useFinancialConfig();
|
|
6678
|
-
const { currency } = config;
|
|
6679
7317
|
const parentLabel = backLabel ?? (direction === "debit" ? t2("financial.nav.payables") : t2("financial.nav.receivables"));
|
|
6680
7318
|
const provider = useFinancialProvider();
|
|
6681
7319
|
const paymentMethods2 = useFinancialStore((s) => s.paymentMethods);
|
|
@@ -6687,6 +7325,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6687
7325
|
const [detailsLoaded, setDetailsLoaded] = useState(false);
|
|
6688
7326
|
const [detailsLoading, setDetailsLoading] = useState(false);
|
|
6689
7327
|
const [invoice, setInvoice] = useState(null);
|
|
7328
|
+
const currency = useCurrency(config.currency, invoice?.currency);
|
|
6690
7329
|
const unitMode = useUnitMode();
|
|
6691
7330
|
const unitName = useUnitStore((st) => unitMode && invoice?.unitId ? st.byId[invoice.unitId]?.name : void 0);
|
|
6692
7331
|
const [items, setItems] = useState([]);
|
|
@@ -6723,6 +7362,66 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6723
7362
|
void loadData();
|
|
6724
7363
|
}
|
|
6725
7364
|
}
|
|
7365
|
+
const [pinnedAmounts, setPinnedAmounts] = useState(/* @__PURE__ */ new Set());
|
|
7366
|
+
const [showCancelledRows, setShowCancelledRows] = useState(false);
|
|
7367
|
+
const [receiveTarget, setReceiveTarget] = useState(null);
|
|
7368
|
+
const [stockDetailTarget, setStockDetailTarget] = useState(null);
|
|
7369
|
+
const [stockDetails, setStockDetails] = useState(/* @__PURE__ */ new Map());
|
|
7370
|
+
const stockSummary = (itemId, total) => {
|
|
7371
|
+
const named = (stockDetails.get(itemId) ?? []).filter((d) => d.variantName && d.variantName !== "Padr\xE3o");
|
|
7372
|
+
const count = total.toLocaleString("pt-BR");
|
|
7373
|
+
return named.length ? `${count} (${named.map((d) => `${d.quantity.toLocaleString("pt-BR")} ${d.variantName}`).join(", ")})` : count;
|
|
7374
|
+
};
|
|
7375
|
+
const [stockVersion, setStockVersion] = useState(0);
|
|
7376
|
+
const [stockByItem, setStockByItem] = useState(null);
|
|
7377
|
+
useEffect(() => {
|
|
7378
|
+
let alive = true;
|
|
7379
|
+
setStockByItem(null);
|
|
7380
|
+
if (direction !== "debit" || !provider.getInvoiceStockEntries) return;
|
|
7381
|
+
provider.getInvoiceStockEntries(invoiceId).then((entries) => {
|
|
7382
|
+
if (!alive) return;
|
|
7383
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
7384
|
+
for (const e of entries) byItem.set(e.itemId, (byItem.get(e.itemId) ?? 0) + e.entered);
|
|
7385
|
+
setStockByItem(byItem);
|
|
7386
|
+
}).catch(() => {
|
|
7387
|
+
if (alive) setStockByItem(/* @__PURE__ */ new Map());
|
|
7388
|
+
});
|
|
7389
|
+
void provider.getInvoiceStockDetails?.(invoiceId).then((rows) => {
|
|
7390
|
+
if (!alive) return;
|
|
7391
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
7392
|
+
for (const r of rows) byItem.set(r.itemId, [...byItem.get(r.itemId) ?? [], r]);
|
|
7393
|
+
setStockDetails(byItem);
|
|
7394
|
+
}).catch(() => {
|
|
7395
|
+
});
|
|
7396
|
+
return () => {
|
|
7397
|
+
alive = false;
|
|
7398
|
+
};
|
|
7399
|
+
}, [provider, invoiceId, direction, stockVersion]);
|
|
7400
|
+
const canChangeAmounts = !!provider.rescheduleInvoice && invoice?.status !== "cancelled" && movements.length > 1 && movements.every((m) => m.status !== "paid" && m.status !== "partial" && m.status !== "cancelled" && !(m.paidAmount > 0));
|
|
7401
|
+
async function patchAmount(mov, value) {
|
|
7402
|
+
if (!invoice || !provider.rescheduleInvoice) return;
|
|
7403
|
+
const index = movements.findIndex((m) => m.id === mov.id);
|
|
7404
|
+
const next = rebalanceInstallments({
|
|
7405
|
+
amounts: movements.map((m) => m.amount),
|
|
7406
|
+
index,
|
|
7407
|
+
value,
|
|
7408
|
+
total: invoice.totalAmount,
|
|
7409
|
+
pinned: movements.map((m) => pinnedAmounts.has(m.installmentNumber ?? 0))
|
|
7410
|
+
});
|
|
7411
|
+
if (!next) return;
|
|
7412
|
+
setPinnedAmounts((prev) => new Set(prev).add(mov.installmentNumber ?? 0));
|
|
7413
|
+
setMovements((prev) => prev.map((m, n) => ({ ...m, amount: next[n] })));
|
|
7414
|
+
try {
|
|
7415
|
+
await provider.rescheduleInvoice(invoice.id, movements.map((m, n) => ({
|
|
7416
|
+
dueDate: m.dueDate,
|
|
7417
|
+
amount: next[n],
|
|
7418
|
+
metadata: m.metadata ?? {}
|
|
7419
|
+
})));
|
|
7420
|
+
} catch (err) {
|
|
7421
|
+
toast.error("N\xE3o foi poss\xEDvel mudar o valor da parcela", { description: err?.message });
|
|
7422
|
+
}
|
|
7423
|
+
void loadData();
|
|
7424
|
+
}
|
|
6726
7425
|
async function patchInvoiceDate(invoiceDate) {
|
|
6727
7426
|
if (!invoice || !invoiceDate || invoiceDate === invoice.invoiceDate) return;
|
|
6728
7427
|
const antes = invoice;
|
|
@@ -6808,8 +7507,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6808
7507
|
useEffect(() => {
|
|
6809
7508
|
void loadPaymentDetails();
|
|
6810
7509
|
}, [invoiceId]);
|
|
6811
|
-
const [chartNames, setChartNames] =
|
|
6812
|
-
|
|
7510
|
+
const [chartNames, setChartNames] = React29__default.useState(/* @__PURE__ */ new Map());
|
|
7511
|
+
React29__default.useEffect(() => {
|
|
6813
7512
|
let cancelled2 = false;
|
|
6814
7513
|
void provider.getChartOfAccounts().then((rows) => {
|
|
6815
7514
|
if (!cancelled2) setChartNames(new Map(rows.map((r) => [r.id, r.name])));
|
|
@@ -6845,6 +7544,10 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6845
7544
|
const statusConfig = STATUS_COLORS[invoice.status] ?? STATUS_COLORS.pending;
|
|
6846
7545
|
statusConfig.icon;
|
|
6847
7546
|
const totalPaid = movements.reduce((sum, m) => sum + m.paidAmount, 0);
|
|
7547
|
+
const liveInstallments = movements.filter((m) => m.status !== "cancelled");
|
|
7548
|
+
const splitCancelled = invoice.status !== "cancelled" && liveInstallments.length > 0;
|
|
7549
|
+
const scheduleRows = splitCancelled ? liveInstallments : movements;
|
|
7550
|
+
const cancelledRows = splitCancelled ? movements.filter((m) => m.status === "cancelled") : [];
|
|
6848
7551
|
const totalRemaining = invoice.totalAmount - totalPaid;
|
|
6849
7552
|
const counted = items.filter((it) => !it.cancelledAt);
|
|
6850
7553
|
const subtotal = counted.reduce((sum, it) => sum + it.quantity * it.unitPrice, 0);
|
|
@@ -6865,13 +7568,31 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6865
7568
|
// no meio da linha — o mesmo lugar que ela ocupa no formulário.
|
|
6866
7569
|
account: it.accountId ? chartNames.get(it.accountId) : void 0,
|
|
6867
7570
|
icon: ITEM_ICONS[it.itemKind ?? "other"],
|
|
7571
|
+
imageUrl: it.imageUrl,
|
|
7572
|
+
note: stockDetails.has(it.id) ? /* @__PURE__ */ jsx(StockEntryNote, { entries: stockDetails.get(it.id), onOpen: () => setStockDetailTarget(it) }) : void 0,
|
|
6868
7573
|
// O LÁPIS DA LINHA. Aparece no hover (o arquétipo reserva o espaço, então
|
|
6869
7574
|
// a coluna de valores não dança) e leva para a edição com ESTA linha
|
|
6870
7575
|
// aberta — sem ele, conferir um item e querer corrigi-lo custava abrir a
|
|
6871
7576
|
// edição e reencontrar ali o que o dedo já estava apontando.
|
|
6872
7577
|
...invoice.status !== "cancelled" && !it.cancelledAt ? { actions: [
|
|
6873
7578
|
...onEdit && movements.every((m) => m.paidAmount <= 0) ? [{ id: "edit", label: t2("financial.invoiceForm.edit"), icon: Pencil, onSelect: () => onEdit(i) }] : [],
|
|
6874
|
-
|
|
7579
|
+
// What already entered is stated; receiving (more) is always offered,
|
|
7580
|
+
// because goods rarely arrive the way the bill reads.
|
|
7581
|
+
...direction === "debit" && stockByItem && stockByItem.has(it.id) ? [{
|
|
7582
|
+
id: "stock-status",
|
|
7583
|
+
icon: Package,
|
|
7584
|
+
disabled: true,
|
|
7585
|
+
onSelect: () => {
|
|
7586
|
+
},
|
|
7587
|
+
label: `No estoque: ${stockSummary(it.id, stockByItem.get(it.id))}`
|
|
7588
|
+
}] : [],
|
|
7589
|
+
...direction === "debit" && stockByItem && provider.receiveInvoiceItemStock ? [{
|
|
7590
|
+
id: "stock",
|
|
7591
|
+
icon: PackagePlus,
|
|
7592
|
+
onSelect: () => setReceiveTarget(it),
|
|
7593
|
+
label: stockByItem.has(it.id) ? "Dar entrada em mais" : "Dar entrada no estoque"
|
|
7594
|
+
}] : [],
|
|
7595
|
+
{ id: "cancel", label: "Cancelar", icon: Ban, intent: "danger", onSelect: () => setCancelTarget(it.id) }
|
|
6875
7596
|
] } : {},
|
|
6876
7597
|
quantity: it.quantity,
|
|
6877
7598
|
unitPrice: formatCurrency(it.unitPrice, currency),
|
|
@@ -6916,7 +7637,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6916
7637
|
]
|
|
6917
7638
|
}
|
|
6918
7639
|
) }),
|
|
6919
|
-
invoice.observations && /* @__PURE__ */ jsx(OrderDetailBlock, { title: t2("financial.invoice.notes"), children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: invoice.observations }) })
|
|
7640
|
+
invoice.observations && /* @__PURE__ */ jsx(OrderDetailBlock, { title: t2("financial.invoice.notes"), children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: invoice.observations }) }),
|
|
7641
|
+
!!invoice.tags?.length && /* @__PURE__ */ jsx(OrderDetailBlock, { title: t2("financial.invoiceForm.tags"), children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: invoice.tags.map((tag) => /* @__PURE__ */ jsx(TagChip, { tag }, tag)) }) })
|
|
6920
7642
|
] });
|
|
6921
7643
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6922
7644
|
/* @__PURE__ */ jsx(
|
|
@@ -6947,134 +7669,152 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
6947
7669
|
customer: { name: invoice.contactName, email: null },
|
|
6948
7670
|
customerNode: invoice.contactName ? /* @__PURE__ */ jsx(PersonLink, { personId: invoice.contactId, name: invoice.contactName, size: "sm" }) : /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" }),
|
|
6949
7671
|
items: detailItems,
|
|
7672
|
+
itemActionsMenu: true,
|
|
6950
7673
|
itemsLabel: t2("financial.invoice.items"),
|
|
6951
7674
|
totals: totalRows,
|
|
6952
7675
|
totalsLabel: t2("financial.invoice.payment"),
|
|
6953
7676
|
mainSections: (
|
|
6954
7677
|
// O cronograma no MESMO bloco que os itens e o pagamento — antes o título
|
|
6955
7678
|
// ficava fora do cartão, e a seção lia como se fosse de outra tela.
|
|
6956
|
-
/* @__PURE__ */
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
{
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
});
|
|
7015
|
-
}
|
|
7016
|
-
}
|
|
7017
|
-
) : void 0,
|
|
7018
|
-
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7019
|
-
paymentSummary && /* @__PURE__ */ jsx(
|
|
7020
|
-
"button",
|
|
7021
|
-
{
|
|
7022
|
-
type: "button",
|
|
7023
|
-
onClick: () => {
|
|
7024
|
-
if (hasPaidDetails) setReceiptBill(mov);
|
|
7025
|
-
},
|
|
7026
|
-
title: paymentSummary,
|
|
7027
|
-
"aria-label": paymentSummary,
|
|
7028
|
-
className: "inline-flex shrink-0 items-center rounded-md px-1 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
7029
|
-
children: howCard ? /* @__PURE__ */ jsx(CardVisual, { lastFour: howCard.lastFour ?? void 0, color: howCard.color ?? void 0, brandId: howCard.cardBrandId ?? void 0 }) : HowIcon ? /* @__PURE__ */ jsx(HowIcon, { className: "h-4 w-4", "aria-hidden": true }) : null
|
|
7030
|
-
}
|
|
7031
|
-
),
|
|
7032
|
-
/* @__PURE__ */ jsx(
|
|
7033
|
-
InstallmentReceiptButton,
|
|
7034
|
-
{
|
|
7035
|
-
title: "Anexos",
|
|
7036
|
-
attached: attachments.length > 0,
|
|
7037
|
-
count: attachments.length,
|
|
7038
|
-
onClick: () => setAttachmentsOpen(true)
|
|
7039
|
-
}
|
|
7040
|
-
),
|
|
7041
|
-
!isPaid2 && !isPartial && ativa && /* @__PURE__ */ jsxs(
|
|
7042
|
-
InstallmentPayButton,
|
|
7679
|
+
/* @__PURE__ */ jsx(
|
|
7680
|
+
OrderDetailBlock,
|
|
7681
|
+
{
|
|
7682
|
+
title: t2("financial.invoice.paymentSchedule"),
|
|
7683
|
+
actions: attachments.length > 0 ? /* @__PURE__ */ jsxs(
|
|
7684
|
+
"button",
|
|
7685
|
+
{
|
|
7686
|
+
type: "button",
|
|
7687
|
+
onClick: () => setAttachmentsOpen(true),
|
|
7688
|
+
title: attachments.map((a) => a.fileName).join("\n"),
|
|
7689
|
+
className: "-my-1 inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
7690
|
+
children: [
|
|
7691
|
+
/* @__PURE__ */ jsx(Paperclip, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
7692
|
+
attachments.length === 1 ? "Ver o anexo" : `Ver os ${attachments.length} anexos`
|
|
7693
|
+
]
|
|
7694
|
+
}
|
|
7695
|
+
) : void 0,
|
|
7696
|
+
children: /* @__PURE__ */ jsxs("div", { className: "-mx-4 -mb-4", children: [
|
|
7697
|
+
movements.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-4 py-4 text-center text-sm text-muted-foreground", children: t2("financial.invoice.noInstallments") }) : /* @__PURE__ */ jsx("div", { className: "divide-y border-t", children: [...scheduleRows, ...showCancelledRows ? cancelledRows : []].map((mov) => {
|
|
7698
|
+
const remaining = mov.amount - mov.paidAmount;
|
|
7699
|
+
const isPaid2 = mov.status === "paid";
|
|
7700
|
+
const isPartial = mov.status === "partial";
|
|
7701
|
+
const hasPaidDetails = isPaid2 || isPartial;
|
|
7702
|
+
const instPayments = settlements.byBillId.get(mov.id) ?? [];
|
|
7703
|
+
const hows = instPayments.map((payment) => describePayment(payment, howCtx));
|
|
7704
|
+
const paymentSummary = [...new Set(hows.map((how2) => [how2.method, how2.account?.name].filter(Boolean).join(" \xB7 ")).filter(Boolean))].join(" / ");
|
|
7705
|
+
const how = hows[0];
|
|
7706
|
+
const howCard = how?.account?.accountType === "card_payable" ? how.account : void 0;
|
|
7707
|
+
const HowIcon = METHOD_ICONS[how?.typeKind ?? ""] ?? (how ? CreditCard : null);
|
|
7708
|
+
const srcMeta = mov.metadata ?? {};
|
|
7709
|
+
const srcStr = (v) => typeof v === "string" && v ? v : void 0;
|
|
7710
|
+
const srcTypeId = srcStr(srcMeta.payMethodTypeId);
|
|
7711
|
+
const srcAccountId = srcStr(srcMeta.payAccountId);
|
|
7712
|
+
const editavel = !hasPaidDetails && invoice.status !== "cancelled";
|
|
7713
|
+
const canChooseSource = editavel && Math.abs(mov.amount) >= 5e-3;
|
|
7714
|
+
const faltaParaPagar = !srcTypeId ? t2("financial.payment.methodRequired") : !srcAccountId ? t2("financial.payment.accountRequired") : null;
|
|
7715
|
+
const ativa = invoice.status !== "cancelled";
|
|
7716
|
+
return /* @__PURE__ */ jsx(
|
|
7717
|
+
InstallmentRow,
|
|
7718
|
+
{
|
|
7719
|
+
number: mov.installmentNumber ?? 1,
|
|
7720
|
+
amount: formatCurrency(mov.amount, currency),
|
|
7721
|
+
amountValue: mov.amount,
|
|
7722
|
+
onAmountChange: canChangeAmounts ? (v) => {
|
|
7723
|
+
void patchAmount(mov, v);
|
|
7724
|
+
} : void 0,
|
|
7725
|
+
status: mov.status,
|
|
7726
|
+
dueDate: mov.dueDate,
|
|
7727
|
+
onDueDateChange: editavel ? (v) => {
|
|
7728
|
+
void patchDueDate(mov, v);
|
|
7729
|
+
} : void 0,
|
|
7730
|
+
dueExtra: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7731
|
+
mov.paidAmount > 0 && !isPaid2 && ` \xB7 ${t2("financial.invoice.paidLabel")} ${formatCurrency(mov.paidAmount, currency)}`,
|
|
7732
|
+
mov.paymentDate && ` \xB7 ${t2("financial.invoice.paidOn")} ${mov.paymentDate}`
|
|
7733
|
+
] }),
|
|
7734
|
+
onClick: hasPaidDetails ? () => setReceiptBill(mov) : void 0,
|
|
7735
|
+
source: canChooseSource ? /* @__PURE__ */ jsx(
|
|
7736
|
+
InstallmentSourceFields,
|
|
7043
7737
|
{
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7738
|
+
direction,
|
|
7739
|
+
value: { payMethodTypeId: srcTypeId, payAccountId: srcAccountId },
|
|
7740
|
+
onChange: (next) => {
|
|
7741
|
+
void patchSource(mov, {
|
|
7742
|
+
payMethodTypeId: next.payMethodTypeId ?? null,
|
|
7743
|
+
payAccountId: next.payAccountId ?? null
|
|
7744
|
+
});
|
|
7745
|
+
}
|
|
7052
7746
|
}
|
|
7053
|
-
),
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7747
|
+
) : void 0,
|
|
7748
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7749
|
+
paymentSummary && /* @__PURE__ */ jsx(
|
|
7750
|
+
"button",
|
|
7751
|
+
{
|
|
7752
|
+
type: "button",
|
|
7753
|
+
onClick: () => {
|
|
7754
|
+
if (hasPaidDetails) setReceiptBill(mov);
|
|
7755
|
+
},
|
|
7756
|
+
title: paymentSummary,
|
|
7757
|
+
"aria-label": paymentSummary,
|
|
7758
|
+
className: "inline-flex shrink-0 items-center rounded-md px-1 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
7759
|
+
children: howCard ? /* @__PURE__ */ jsx(CardVisual, { lastFour: howCard.lastFour ?? void 0, color: howCard.color ?? void 0, brandId: howCard.cardBrandId ?? void 0 }) : HowIcon ? /* @__PURE__ */ jsx(HowIcon, { className: "h-4 w-4", "aria-hidden": true }) : null
|
|
7760
|
+
}
|
|
7761
|
+
),
|
|
7762
|
+
/* @__PURE__ */ jsx(
|
|
7763
|
+
InstallmentReceiptButton,
|
|
7764
|
+
{
|
|
7765
|
+
title: "Anexos",
|
|
7766
|
+
attached: attachments.length > 0,
|
|
7767
|
+
count: attachments.length,
|
|
7768
|
+
onClick: () => setAttachmentsOpen(true)
|
|
7769
|
+
}
|
|
7770
|
+
),
|
|
7771
|
+
!isPaid2 && !isPartial && ativa && /* @__PURE__ */ jsxs(
|
|
7772
|
+
InstallmentPayButton,
|
|
7773
|
+
{
|
|
7774
|
+
onClick: () => setPayingMovement(mov),
|
|
7775
|
+
disabled: !!faltaParaPagar,
|
|
7776
|
+
title: faltaParaPagar ?? void 0,
|
|
7777
|
+
children: [
|
|
7778
|
+
t2("financial.invoice.pay"),
|
|
7779
|
+
" ",
|
|
7780
|
+
remaining < mov.amount ? formatCurrency(remaining, currency) : ""
|
|
7781
|
+
]
|
|
7782
|
+
}
|
|
7783
|
+
),
|
|
7784
|
+
isPaid2 && reversiblePayments(instPayments).length > 0 && ativa && /* @__PURE__ */ jsx(InstallmentActionButton, { icon: Undo2, onClick: () => setReversing(reversiblePayments(instPayments).at(-1)), children: t2("financial.invoice.reverse") }),
|
|
7785
|
+
isPartial && ativa && /* @__PURE__ */ jsxs(InstallmentPayButton, { variant: "outline", onClick: () => setPayingMovement(mov), children: [
|
|
7786
|
+
t2("financial.invoice.pay"),
|
|
7787
|
+
" ",
|
|
7788
|
+
formatCurrency(remaining, currency)
|
|
7789
|
+
] })
|
|
7059
7790
|
] })
|
|
7791
|
+
},
|
|
7792
|
+
mov.id
|
|
7793
|
+
);
|
|
7794
|
+
}) }),
|
|
7795
|
+
cancelledRows.length > 0 && /* @__PURE__ */ jsx(
|
|
7796
|
+
"button",
|
|
7797
|
+
{
|
|
7798
|
+
type: "button",
|
|
7799
|
+
onClick: () => setShowCancelledRows((v) => !v),
|
|
7800
|
+
className: "w-full border-t px-4 py-2 text-left text-xs text-muted-foreground transition-colors hover:bg-muted/30 hover:text-foreground",
|
|
7801
|
+
children: showCancelledRows ? "Ocultar parcelas canceladas" : cancelledRows.length === 1 ? "Ver 1 parcela cancelada" : `Ver ${cancelledRows.length} parcelas canceladas`
|
|
7802
|
+
}
|
|
7803
|
+
),
|
|
7804
|
+
settlements.unattributed.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-t bg-warning-soft/40 px-4 py-2.5", children: [
|
|
7805
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-foreground", children: t2("financial.invoice.unlinkedPayments") }),
|
|
7806
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[10px] text-muted-foreground", children: t2("financial.invoice.unlinkedPaymentsDesc") }),
|
|
7807
|
+
settlements.unattributed.map((p) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
7808
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-success", children: formatCurrency(p.paidAmount, currency) }),
|
|
7809
|
+
p.paymentDate && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7810
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
7811
|
+
/* @__PURE__ */ jsx("span", { children: p.paymentDate })
|
|
7060
7812
|
] })
|
|
7061
|
-
},
|
|
7062
|
-
|
|
7063
|
-
);
|
|
7064
|
-
}) }),
|
|
7065
|
-
settlements.unattributed.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-t bg-warning-soft/40 px-4 py-2.5", children: [
|
|
7066
|
-
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-foreground", children: t2("financial.invoice.unlinkedPayments") }),
|
|
7067
|
-
/* @__PURE__ */ jsx("p", { className: "mb-1 text-[10px] text-muted-foreground", children: t2("financial.invoice.unlinkedPaymentsDesc") }),
|
|
7068
|
-
settlements.unattributed.map((p) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
7069
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-success", children: formatCurrency(p.paidAmount, currency) }),
|
|
7070
|
-
p.paymentDate && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7071
|
-
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
7072
|
-
/* @__PURE__ */ jsx("span", { children: p.paymentDate })
|
|
7073
|
-
] })
|
|
7074
|
-
] }, p.id))
|
|
7813
|
+
] }, p.id))
|
|
7814
|
+
] })
|
|
7075
7815
|
] })
|
|
7076
|
-
|
|
7077
|
-
|
|
7816
|
+
}
|
|
7817
|
+
)
|
|
7078
7818
|
),
|
|
7079
7819
|
extraSections: sideBlocks
|
|
7080
7820
|
}
|
|
@@ -7152,7 +7892,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
7152
7892
|
]
|
|
7153
7893
|
}
|
|
7154
7894
|
),
|
|
7155
|
-
payingMovement && /* @__PURE__ */ jsx(
|
|
7895
|
+
payingMovement && /* @__PURE__ */ jsx(CurrencyScope, { code: invoice?.currency, children: /* @__PURE__ */ jsx(
|
|
7156
7896
|
PaymentModal,
|
|
7157
7897
|
{
|
|
7158
7898
|
movement: payingMovement,
|
|
@@ -7167,8 +7907,30 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
7167
7907
|
loadData();
|
|
7168
7908
|
}
|
|
7169
7909
|
}
|
|
7910
|
+
) }),
|
|
7911
|
+
stockDetailTarget && stockDetails.has(stockDetailTarget.id) && /* @__PURE__ */ jsx(
|
|
7912
|
+
StockEntryDetailsDialog,
|
|
7913
|
+
{
|
|
7914
|
+
title: stockDetailTarget.description,
|
|
7915
|
+
entries: stockDetails.get(stockDetailTarget.id),
|
|
7916
|
+
onClose: () => setStockDetailTarget(null)
|
|
7917
|
+
}
|
|
7918
|
+
),
|
|
7919
|
+
receiveTarget && /* @__PURE__ */ jsx(
|
|
7920
|
+
ReceiveStockDialog,
|
|
7921
|
+
{
|
|
7922
|
+
item: receiveTarget,
|
|
7923
|
+
invoiceCurrency: invoice.currency,
|
|
7924
|
+
entered: stockDetails.get(receiveTarget.id),
|
|
7925
|
+
onClose: () => setReceiveTarget(null),
|
|
7926
|
+
onReceived: () => {
|
|
7927
|
+
setReceiveTarget(null);
|
|
7928
|
+
setStockVersion((v) => v + 1);
|
|
7929
|
+
void loadData();
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7170
7932
|
),
|
|
7171
|
-
cancelTarget !== null && /* @__PURE__ */ jsx(
|
|
7933
|
+
cancelTarget !== null && /* @__PURE__ */ jsx(CurrencyScope, { code: invoice?.currency, children: /* @__PURE__ */ jsx(
|
|
7172
7934
|
CancelInvoiceDialog,
|
|
7173
7935
|
{
|
|
7174
7936
|
invoiceId: invoice.id,
|
|
@@ -7183,7 +7945,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
|
|
|
7183
7945
|
void loadData();
|
|
7184
7946
|
}
|
|
7185
7947
|
}
|
|
7186
|
-
),
|
|
7948
|
+
) }),
|
|
7187
7949
|
/* @__PURE__ */ jsx("style", { children: `
|
|
7188
7950
|
@keyframes field-slide-in {
|
|
7189
7951
|
from { opacity: 0; transform: translateY(8px); }
|
|
@@ -7445,8 +8207,8 @@ function DenominationCounter({
|
|
|
7445
8207
|
onChange({ ...counts, [value]: Math.max(0, count) });
|
|
7446
8208
|
}
|
|
7447
8209
|
function renderRow(d) {
|
|
7448
|
-
const
|
|
7449
|
-
const subtotal =
|
|
8210
|
+
const qty2 = counts[d.value] ?? 0;
|
|
8211
|
+
const subtotal = qty2 * d.value;
|
|
7450
8212
|
return (
|
|
7451
8213
|
// Larguras fixas somavam ~304px por coluna; dentro do diálogo a coluna
|
|
7452
8214
|
// tem ~200px e "R$ 200" quebrava em duas linhas, deixando as notas com
|
|
@@ -7461,7 +8223,7 @@ function DenominationCounter({
|
|
|
7461
8223
|
{
|
|
7462
8224
|
type: "number",
|
|
7463
8225
|
min: 0,
|
|
7464
|
-
value:
|
|
8226
|
+
value: qty2 || "",
|
|
7465
8227
|
onChange: (e) => setCount(d.value, parseInt(e.target.value) || 0),
|
|
7466
8228
|
placeholder: "0",
|
|
7467
8229
|
className: "w-12 shrink-0 rounded border bg-background px-1.5 py-1 text-xs text-center tabular-nums"
|
|
@@ -7795,7 +8557,7 @@ function CashRegistersView({ variant = "page" } = {}) {
|
|
|
7795
8557
|
const [tab, setTab] = useState("open");
|
|
7796
8558
|
const [reviewRows, setReviewRows] = useState(null);
|
|
7797
8559
|
const [reviewError, setReviewError] = useState(null);
|
|
7798
|
-
const loadReviews =
|
|
8560
|
+
const loadReviews = React29__default.useCallback(async () => {
|
|
7799
8561
|
try {
|
|
7800
8562
|
setReviewError(null);
|
|
7801
8563
|
setReviewRows(await provider.getCashSessionReviews?.() ?? []);
|
|
@@ -8395,7 +9157,7 @@ function StatementsView({ onNavigate }) {
|
|
|
8395
9157
|
"extrato"
|
|
8396
9158
|
);
|
|
8397
9159
|
};
|
|
8398
|
-
const headerActions =
|
|
9160
|
+
const headerActions = React29__default.useMemo(() => [{
|
|
8399
9161
|
id: "new-transfer",
|
|
8400
9162
|
label: t2("financial.statements.newTransfer"),
|
|
8401
9163
|
icon: ArrowRightLeft,
|
|
@@ -8591,15 +9353,15 @@ function CommissionsView() {
|
|
|
8591
9353
|
const fetchCommissions = useFinancialStore((s) => s.fetchCommissions);
|
|
8592
9354
|
const generateCommissions = useFinancialStore((s) => s.generateCommissions);
|
|
8593
9355
|
const provider = useFinancialProvider();
|
|
8594
|
-
const [from, setFrom] =
|
|
8595
|
-
const [to, setTo] =
|
|
8596
|
-
const [preset, setPreset] =
|
|
8597
|
-
const [running, setRunning] =
|
|
8598
|
-
const [lastRun, setLastRun] =
|
|
8599
|
-
const [generation, setGeneration] =
|
|
8600
|
-
const [busy, setBusy] =
|
|
8601
|
-
const [error, setError] =
|
|
8602
|
-
const loadGeneration =
|
|
9356
|
+
const [from, setFrom] = React29__default.useState(firstOfMonth());
|
|
9357
|
+
const [to, setTo] = React29__default.useState(today2());
|
|
9358
|
+
const [preset, setPreset] = React29__default.useState("this_month");
|
|
9359
|
+
const [running, setRunning] = React29__default.useState(false);
|
|
9360
|
+
const [lastRun, setLastRun] = React29__default.useState(null);
|
|
9361
|
+
const [generation, setGeneration] = React29__default.useState(null);
|
|
9362
|
+
const [busy, setBusy] = React29__default.useState(null);
|
|
9363
|
+
const [error, setError] = React29__default.useState(null);
|
|
9364
|
+
const loadGeneration = React29__default.useCallback(async () => {
|
|
8603
9365
|
try {
|
|
8604
9366
|
const gens = await provider.getCommissionGenerations?.() ?? [];
|
|
8605
9367
|
const doPeriodo = gens.filter((g) => g.periodStart === from && g.periodEnd === to);
|
|
@@ -8607,11 +9369,11 @@ function CommissionsView() {
|
|
|
8607
9369
|
} catch {
|
|
8608
9370
|
}
|
|
8609
9371
|
}, [provider, from, to]);
|
|
8610
|
-
|
|
9372
|
+
React29__default.useEffect(() => {
|
|
8611
9373
|
void fetchCommissions({ periodStart: from, periodEnd: to });
|
|
8612
9374
|
void loadGeneration();
|
|
8613
9375
|
}, [fetchCommissions, loadGeneration, from, to]);
|
|
8614
|
-
const byPerson =
|
|
9376
|
+
const byPerson = React29__default.useMemo(() => {
|
|
8615
9377
|
const map = /* @__PURE__ */ new Map();
|
|
8616
9378
|
for (const c of commissions2) {
|
|
8617
9379
|
const key = c.personId;
|
|
@@ -8664,7 +9426,7 @@ function CommissionsView() {
|
|
|
8664
9426
|
setBusy(null);
|
|
8665
9427
|
}
|
|
8666
9428
|
}
|
|
8667
|
-
const headerActions =
|
|
9429
|
+
const headerActions = React29__default.useMemo(() => [{
|
|
8668
9430
|
id: "commission-rules",
|
|
8669
9431
|
label: "Configurar regras de comiss\xE3o",
|
|
8670
9432
|
icon: Settings,
|
|
@@ -9112,13 +9874,13 @@ function money3(v) {
|
|
|
9112
9874
|
function GoalsView() {
|
|
9113
9875
|
const { labels } = useFinancialConfig();
|
|
9114
9876
|
const provider = useFinancialProvider();
|
|
9115
|
-
const [units, setUnits] =
|
|
9116
|
-
const [unitId, setUnitId] =
|
|
9117
|
-
const [year, setYear] =
|
|
9118
|
-
const [rows, setRows] =
|
|
9119
|
-
const [editing, setEditing] =
|
|
9120
|
-
const [error, setError] =
|
|
9121
|
-
|
|
9877
|
+
const [units, setUnits] = React29__default.useState([]);
|
|
9878
|
+
const [unitId, setUnitId] = React29__default.useState("");
|
|
9879
|
+
const [year, setYear] = React29__default.useState((/* @__PURE__ */ new Date()).getFullYear());
|
|
9880
|
+
const [rows, setRows] = React29__default.useState(null);
|
|
9881
|
+
const [editing, setEditing] = React29__default.useState(null);
|
|
9882
|
+
const [error, setError] = React29__default.useState(null);
|
|
9883
|
+
React29__default.useEffect(() => {
|
|
9122
9884
|
void (async () => {
|
|
9123
9885
|
const cat = await provider.getCommissionCatalogs?.();
|
|
9124
9886
|
const us = cat?.units ?? [];
|
|
@@ -9126,7 +9888,7 @@ function GoalsView() {
|
|
|
9126
9888
|
setUnitId((cur) => cur || us[0]?.id || "");
|
|
9127
9889
|
})();
|
|
9128
9890
|
}, [provider]);
|
|
9129
|
-
const load =
|
|
9891
|
+
const load = React29__default.useCallback(async () => {
|
|
9130
9892
|
if (!unitId) return;
|
|
9131
9893
|
setRows(null);
|
|
9132
9894
|
try {
|
|
@@ -9137,7 +9899,7 @@ function GoalsView() {
|
|
|
9137
9899
|
setRows([]);
|
|
9138
9900
|
}
|
|
9139
9901
|
}, [provider, unitId, year]);
|
|
9140
|
-
|
|
9902
|
+
React29__default.useEffect(() => {
|
|
9141
9903
|
void load();
|
|
9142
9904
|
}, [load]);
|
|
9143
9905
|
const totalRealizado = (rows ?? []).reduce((s, r) => s + r.achievedAmount, 0);
|
|
@@ -9268,11 +10030,11 @@ function GoalDialog({
|
|
|
9268
10030
|
onSaved
|
|
9269
10031
|
}) {
|
|
9270
10032
|
const provider = useFinancialProvider();
|
|
9271
|
-
const [target, setTarget] =
|
|
9272
|
-
const [sup, setSup] =
|
|
9273
|
-
const [hyper, setHyper] =
|
|
9274
|
-
const [saving, setSaving] =
|
|
9275
|
-
const [error, setError] =
|
|
10033
|
+
const [target, setTarget] = React29__default.useState(String(goal.targetAmount || ""));
|
|
10034
|
+
const [sup, setSup] = React29__default.useState(String(goal.superAmount || ""));
|
|
10035
|
+
const [hyper, setHyper] = React29__default.useState(String(goal.hyperAmount || ""));
|
|
10036
|
+
const [saving, setSaving] = React29__default.useState(false);
|
|
10037
|
+
const [error, setError] = React29__default.useState(null);
|
|
9276
10038
|
const t2 = Number(target) || 0;
|
|
9277
10039
|
const s = Number(sup) || 0;
|
|
9278
10040
|
const h = Number(hyper) || 0;
|
|
@@ -9363,21 +10125,21 @@ function GiftCardsView() {
|
|
|
9363
10125
|
const provider = useFinancialProvider();
|
|
9364
10126
|
const { currency } = useFinancialConfig();
|
|
9365
10127
|
const can = usePermissionOptional();
|
|
9366
|
-
const [enabled, setEnabled] =
|
|
9367
|
-
const [cards, setCards] =
|
|
9368
|
-
const [error, setError] =
|
|
9369
|
-
const [issuing, setIssuing] =
|
|
9370
|
-
const [issued, setIssued] =
|
|
9371
|
-
const [activating, setActivating] =
|
|
9372
|
-
const [search, setSearch] =
|
|
9373
|
-
const [state, setState] =
|
|
9374
|
-
const [minValue, setMinValue] =
|
|
9375
|
-
const [maxValue, setMaxValue] =
|
|
9376
|
-
const [from, setFrom] =
|
|
9377
|
-
const [to, setTo] =
|
|
9378
|
-
const money4 =
|
|
9379
|
-
const seq =
|
|
9380
|
-
const load =
|
|
10128
|
+
const [enabled, setEnabled] = React29__default.useState(null);
|
|
10129
|
+
const [cards, setCards] = React29__default.useState(null);
|
|
10130
|
+
const [error, setError] = React29__default.useState(null);
|
|
10131
|
+
const [issuing, setIssuing] = React29__default.useState(false);
|
|
10132
|
+
const [issued, setIssued] = React29__default.useState(null);
|
|
10133
|
+
const [activating, setActivating] = React29__default.useState(null);
|
|
10134
|
+
const [search, setSearch] = React29__default.useState("");
|
|
10135
|
+
const [state, setState] = React29__default.useState("");
|
|
10136
|
+
const [minValue, setMinValue] = React29__default.useState("");
|
|
10137
|
+
const [maxValue, setMaxValue] = React29__default.useState("");
|
|
10138
|
+
const [from, setFrom] = React29__default.useState("");
|
|
10139
|
+
const [to, setTo] = React29__default.useState("");
|
|
10140
|
+
const money4 = React29__default.useCallback((v) => formatCurrency(v, currency), [currency]);
|
|
10141
|
+
const seq = React29__default.useRef(0);
|
|
10142
|
+
const load = React29__default.useCallback(async () => {
|
|
9381
10143
|
const mine = ++seq.current;
|
|
9382
10144
|
setCards(null);
|
|
9383
10145
|
setError(null);
|
|
@@ -9398,7 +10160,7 @@ function GiftCardsView() {
|
|
|
9398
10160
|
setCards([]);
|
|
9399
10161
|
}
|
|
9400
10162
|
}, [provider, search, state, minValue, maxValue, from, to]);
|
|
9401
|
-
|
|
10163
|
+
React29__default.useEffect(() => {
|
|
9402
10164
|
void (async () => {
|
|
9403
10165
|
try {
|
|
9404
10166
|
setEnabled(await provider.giftCardsEnabled?.() === true);
|
|
@@ -9407,10 +10169,10 @@ function GiftCardsView() {
|
|
|
9407
10169
|
}
|
|
9408
10170
|
})();
|
|
9409
10171
|
}, [provider]);
|
|
9410
|
-
|
|
10172
|
+
React29__default.useEffect(() => {
|
|
9411
10173
|
if (enabled) void load();
|
|
9412
10174
|
}, [enabled, load]);
|
|
9413
|
-
const columns =
|
|
10175
|
+
const columns = React29__default.useMemo(() => [
|
|
9414
10176
|
{
|
|
9415
10177
|
accessorKey: "code",
|
|
9416
10178
|
header: "C\xF3digo",
|
|
@@ -9591,12 +10353,12 @@ function IssueDialog({
|
|
|
9591
10353
|
onIssued
|
|
9592
10354
|
}) {
|
|
9593
10355
|
const provider = useFinancialProvider();
|
|
9594
|
-
const [quantity, setQuantity] =
|
|
9595
|
-
const [amount, setAmount] =
|
|
9596
|
-
const [expiresAt, setExpiresAt] =
|
|
9597
|
-
const [note, setNote] =
|
|
9598
|
-
const [saving, setSaving] =
|
|
9599
|
-
const [error, setError] =
|
|
10356
|
+
const [quantity, setQuantity] = React29__default.useState("10");
|
|
10357
|
+
const [amount, setAmount] = React29__default.useState("100");
|
|
10358
|
+
const [expiresAt, setExpiresAt] = React29__default.useState("");
|
|
10359
|
+
const [note, setNote] = React29__default.useState("");
|
|
10360
|
+
const [saving, setSaving] = React29__default.useState(false);
|
|
10361
|
+
const [error, setError] = React29__default.useState(null);
|
|
9600
10362
|
const q = Math.trunc(Number(quantity)) || 0;
|
|
9601
10363
|
const a = Number(amount) || 0;
|
|
9602
10364
|
const total = q * a;
|
|
@@ -9694,8 +10456,8 @@ function IssueDialog({
|
|
|
9694
10456
|
] }) });
|
|
9695
10457
|
}
|
|
9696
10458
|
var shortDate = (value) => {
|
|
9697
|
-
const [year, month,
|
|
9698
|
-
return year && month &&
|
|
10459
|
+
const [year, month, day3] = value.split("-");
|
|
10460
|
+
return year && month && day3 ? `${day3}/${month}/${year}` : value;
|
|
9699
10461
|
};
|
|
9700
10462
|
function LinhaDoExtrato({ line, fmt }) {
|
|
9701
10463
|
const isCredit = line.direction === "credit";
|
|
@@ -9979,7 +10741,7 @@ function stripFence(text2) {
|
|
|
9979
10741
|
const end = trimmed.lastIndexOf("}");
|
|
9980
10742
|
return start >= 0 && end > start ? trimmed.slice(start, end + 1) : trimmed;
|
|
9981
10743
|
}
|
|
9982
|
-
var
|
|
10744
|
+
var num3 = (v, fallback = 0) => {
|
|
9983
10745
|
const n = typeof v === "string" ? Number(v.replace(",", ".")) : Number(v);
|
|
9984
10746
|
return Number.isFinite(n) ? n : fallback;
|
|
9985
10747
|
};
|
|
@@ -9998,8 +10760,8 @@ function parseReceiptReply(content) {
|
|
|
9998
10760
|
const item = row ?? {};
|
|
9999
10761
|
const description = str(item.description);
|
|
10000
10762
|
if (!description) return null;
|
|
10001
|
-
const quantity =
|
|
10002
|
-
const unitPrice = item.unitPrice != null ?
|
|
10763
|
+
const quantity = num3(item.quantity, 1) || 1;
|
|
10764
|
+
const unitPrice = item.unitPrice != null ? num3(item.unitPrice) : num3(item.total) / quantity;
|
|
10003
10765
|
return { description, quantity, unitPrice: Math.round(unitPrice * 100) / 100 };
|
|
10004
10766
|
}).filter((i) => i !== null) : [];
|
|
10005
10767
|
const warnings = Array.isArray(raw.warnings) ? raw.warnings.map((w) => str(w)).filter((w) => !!w) : [];
|
|
@@ -10020,8 +10782,8 @@ function createAgentReceiptReader(deps) {
|
|
|
10020
10782
|
};
|
|
10021
10783
|
}
|
|
10022
10784
|
var shortDate2 = (value) => {
|
|
10023
|
-
const [year, month,
|
|
10024
|
-
return year && month &&
|
|
10785
|
+
const [year, month, day3] = value.split("-");
|
|
10786
|
+
return year && month && day3 ? `${day3}/${month}/${year}` : value;
|
|
10025
10787
|
};
|
|
10026
10788
|
var shiftDate = (value, days) => {
|
|
10027
10789
|
const d = /* @__PURE__ */ new Date(`${value}T00:00:00`);
|
|
@@ -10982,8 +11744,8 @@ function ReconcileFlow({ line, initialIntent, initialTarget, initialStep = 0, on
|
|
|
10982
11744
|
}
|
|
10983
11745
|
var PAGE_SIZE = 50;
|
|
10984
11746
|
var shortDate3 = (value) => {
|
|
10985
|
-
const [year, month,
|
|
10986
|
-
return year && month &&
|
|
11747
|
+
const [year, month, day3] = value.split("-");
|
|
11748
|
+
return year && month && day3 ? `${day3}/${month}/${year}` : value;
|
|
10987
11749
|
};
|
|
10988
11750
|
var isoDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
10989
11751
|
var ACCOUNT_GROUPS2 = {
|
|
@@ -11152,11 +11914,11 @@ function ReconciliationView() {
|
|
|
11152
11914
|
setBusy(false);
|
|
11153
11915
|
}
|
|
11154
11916
|
}
|
|
11155
|
-
const RefreshIcon =
|
|
11917
|
+
const RefreshIcon = React29__default.useMemo(
|
|
11156
11918
|
() => ({ className }) => /* @__PURE__ */ jsx(RefreshCw, { className: `${className ?? ""} ${loading ? "animate-spin" : ""}` }),
|
|
11157
11919
|
[loading]
|
|
11158
11920
|
);
|
|
11159
|
-
const headerActions =
|
|
11921
|
+
const headerActions = React29__default.useMemo(() => [{
|
|
11160
11922
|
id: "refresh",
|
|
11161
11923
|
label: t2("financial.reconciliation.refresh"),
|
|
11162
11924
|
icon: RefreshIcon,
|
|
@@ -11562,8 +12324,8 @@ function today3() {
|
|
|
11562
12324
|
}
|
|
11563
12325
|
function dateLabel(value) {
|
|
11564
12326
|
if (!value) return "Sem t\xE9rmino";
|
|
11565
|
-
const [year, month,
|
|
11566
|
-
return year && month &&
|
|
12327
|
+
const [year, month, day3] = value.split("-");
|
|
12328
|
+
return year && month && day3 ? `${day3}/${month}/${year}` : value;
|
|
11567
12329
|
}
|
|
11568
12330
|
function newRecurringItem() {
|
|
11569
12331
|
return { _id: crypto.randomUUID(), itemKind: "other", description: "", quantity: 1, unitPrice: 0 };
|
|
@@ -11590,13 +12352,13 @@ function RecurringBillsList({ direction, onNavigate }) {
|
|
|
11590
12352
|
const provider = useFinancialProvider();
|
|
11591
12353
|
const { currency } = useFinancialConfig();
|
|
11592
12354
|
const can = usePermissionOptional();
|
|
11593
|
-
const [rows, setRows] =
|
|
11594
|
-
const [search, setSearch] =
|
|
11595
|
-
const [status, setStatus] =
|
|
11596
|
-
const [changing, setChanging] =
|
|
12355
|
+
const [rows, setRows] = React29__default.useState(null);
|
|
12356
|
+
const [search, setSearch] = React29__default.useState("");
|
|
12357
|
+
const [status, setStatus] = React29__default.useState("all");
|
|
12358
|
+
const [changing, setChanging] = React29__default.useState(null);
|
|
11597
12359
|
const isPayable = direction === "payable";
|
|
11598
12360
|
const prefix = isPayable ? "payables" : "receivables";
|
|
11599
|
-
const load =
|
|
12361
|
+
const load = React29__default.useCallback(async () => {
|
|
11600
12362
|
setRows(null);
|
|
11601
12363
|
try {
|
|
11602
12364
|
setRows(await provider.getRecurringBills?.(direction) ?? []);
|
|
@@ -11605,10 +12367,10 @@ function RecurringBillsList({ direction, onNavigate }) {
|
|
|
11605
12367
|
toast.error(error?.message ?? "N\xE3o foi poss\xEDvel carregar as recorr\xEAncias.");
|
|
11606
12368
|
}
|
|
11607
12369
|
}, [provider, direction]);
|
|
11608
|
-
|
|
12370
|
+
React29__default.useEffect(() => {
|
|
11609
12371
|
void load();
|
|
11610
12372
|
}, [load]);
|
|
11611
|
-
const filtered =
|
|
12373
|
+
const filtered = React29__default.useMemo(() => {
|
|
11612
12374
|
const term = search.trim().toLowerCase();
|
|
11613
12375
|
const visiveis = (rows ?? []).filter((row) => {
|
|
11614
12376
|
if (status === "active" && !row.isActive) return false;
|
|
@@ -11618,7 +12380,7 @@ function RecurringBillsList({ direction, onNavigate }) {
|
|
|
11618
12380
|
});
|
|
11619
12381
|
return [...visiveis].sort((a, b) => Number(b.isActive) - Number(a.isActive) || (a.dueDay ?? 0) - (b.dueDay ?? 0) || a.description.localeCompare(b.description));
|
|
11620
12382
|
}, [rows, search, status]);
|
|
11621
|
-
const porMes =
|
|
12383
|
+
const porMes = React29__default.useMemo(() => {
|
|
11622
12384
|
const fator = { weekly: 52 / 12, monthly: 1, quarterly: 1 / 3, yearly: 1 / 12 };
|
|
11623
12385
|
const ativas = filtered.filter((row) => row.isActive);
|
|
11624
12386
|
return {
|
|
@@ -11639,7 +12401,7 @@ function RecurringBillsList({ direction, onNavigate }) {
|
|
|
11639
12401
|
setChanging(null);
|
|
11640
12402
|
}
|
|
11641
12403
|
}
|
|
11642
|
-
const columns =
|
|
12404
|
+
const columns = React29__default.useMemo(() => [
|
|
11643
12405
|
{
|
|
11644
12406
|
accessorKey: "description",
|
|
11645
12407
|
header: "Descri\xE7\xE3o",
|
|
@@ -11737,41 +12499,41 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
|
|
|
11737
12499
|
const { locations, currency, itemTypes: configuredItemTypes, entityLookups } = useFinancialConfig();
|
|
11738
12500
|
const t2 = useTranslation();
|
|
11739
12501
|
const isPayable = direction === "payable";
|
|
11740
|
-
const [value, setValue] =
|
|
11741
|
-
const [loaded, setLoaded] =
|
|
11742
|
-
const [contact, setContact] =
|
|
11743
|
-
const [counterpartyAccounting, setCounterpartyAccounting] =
|
|
12502
|
+
const [value, setValue] = React29__default.useState(null);
|
|
12503
|
+
const [loaded, setLoaded] = React29__default.useState(!valueId);
|
|
12504
|
+
const [contact, setContact] = React29__default.useState(null);
|
|
12505
|
+
const [counterpartyAccounting, setCounterpartyAccounting] = React29__default.useState({
|
|
11744
12506
|
allowedAccountIds: [],
|
|
11745
12507
|
limitAccounts: false,
|
|
11746
12508
|
addItemsAutomatically: false
|
|
11747
12509
|
});
|
|
11748
|
-
const accountingPolicyRequest =
|
|
11749
|
-
const counterpartyLookup =
|
|
12510
|
+
const accountingPolicyRequest = React29__default.useRef(0);
|
|
12511
|
+
const counterpartyLookup = React29__default.useMemo(
|
|
11750
12512
|
() => createFinancialCounterpartyLookup(isPayable ? "debit" : "credit"),
|
|
11751
12513
|
[isPayable]
|
|
11752
12514
|
);
|
|
11753
12515
|
const contactCreate = useLookupCreate(counterpartyLookup, (created) => {
|
|
11754
12516
|
setContact({ id: created.id, name: created.label });
|
|
11755
12517
|
});
|
|
11756
|
-
const initialItem =
|
|
11757
|
-
const [items, setItems] =
|
|
11758
|
-
const [expandedItemId, setExpandedItemId] =
|
|
11759
|
-
const [addingItem, setAddingItem] =
|
|
11760
|
-
const [frequency, setFrequency] =
|
|
11761
|
-
const [dueDay, setDueDay] =
|
|
11762
|
-
const [startsOn, setStartsOn] =
|
|
11763
|
-
const [endsOn, setEndsOn] =
|
|
11764
|
-
const [unitId, setUnitId] =
|
|
11765
|
-
const [availableLocations, setAvailableLocations] =
|
|
11766
|
-
const [notes, setNotes] =
|
|
11767
|
-
const [isActive, setIsActive] =
|
|
11768
|
-
const [accounts, setAccounts] =
|
|
11769
|
-
const [costCenters, setCostCenters] =
|
|
12518
|
+
const initialItem = React29__default.useRef(newRecurringItem());
|
|
12519
|
+
const [items, setItems] = React29__default.useState([initialItem.current]);
|
|
12520
|
+
const [expandedItemId, setExpandedItemId] = React29__default.useState(initialItem.current._id);
|
|
12521
|
+
const [addingItem, setAddingItem] = React29__default.useState(false);
|
|
12522
|
+
const [frequency, setFrequency] = React29__default.useState("monthly");
|
|
12523
|
+
const [dueDay, setDueDay] = React29__default.useState("1");
|
|
12524
|
+
const [startsOn, setStartsOn] = React29__default.useState(today3());
|
|
12525
|
+
const [endsOn, setEndsOn] = React29__default.useState("");
|
|
12526
|
+
const [unitId, setUnitId] = React29__default.useState(locations[0]?.id ?? "");
|
|
12527
|
+
const [availableLocations, setAvailableLocations] = React29__default.useState(locations);
|
|
12528
|
+
const [notes, setNotes] = React29__default.useState("");
|
|
12529
|
+
const [isActive, setIsActive] = React29__default.useState(true);
|
|
12530
|
+
const [accounts, setAccounts] = React29__default.useState([]);
|
|
12531
|
+
const [costCenters, setCostCenters] = React29__default.useState([]);
|
|
11770
12532
|
const { choices: unitChoices } = useWritableUnits(availableLocations);
|
|
11771
|
-
const [saving, setSaving] =
|
|
11772
|
-
const [ending, setEnding] =
|
|
11773
|
-
const [error, setError] =
|
|
11774
|
-
|
|
12533
|
+
const [saving, setSaving] = React29__default.useState(false);
|
|
12534
|
+
const [ending, setEnding] = React29__default.useState(false);
|
|
12535
|
+
const [error, setError] = React29__default.useState(null);
|
|
12536
|
+
React29__default.useEffect(() => {
|
|
11775
12537
|
if (locations.length > 0) {
|
|
11776
12538
|
setAvailableLocations(locations);
|
|
11777
12539
|
return;
|
|
@@ -11786,7 +12548,7 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
|
|
|
11786
12548
|
cancelled = true;
|
|
11787
12549
|
};
|
|
11788
12550
|
}, [locations]);
|
|
11789
|
-
|
|
12551
|
+
React29__default.useEffect(() => {
|
|
11790
12552
|
let cancelled = false;
|
|
11791
12553
|
void Promise.all([
|
|
11792
12554
|
provider.getChartOfAccounts(),
|
|
@@ -11833,25 +12595,25 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
|
|
|
11833
12595
|
cancelled = true;
|
|
11834
12596
|
};
|
|
11835
12597
|
}, [provider, direction, valueId]);
|
|
11836
|
-
const translatedItemTypes =
|
|
12598
|
+
const translatedItemTypes = React29__default.useMemo(() => configuredItemTypes.map((tp) => {
|
|
11837
12599
|
const label = t2(`financial.itemType.${tp.value}`);
|
|
11838
12600
|
return label === `financial.itemType.${tp.value}` ? tp : { ...tp, label };
|
|
11839
12601
|
}), [configuredItemTypes, t2]);
|
|
11840
|
-
const offeredItemTypes =
|
|
12602
|
+
const offeredItemTypes = React29__default.useMemo(() => availableInvoiceItemTypes(
|
|
11841
12603
|
translatedItemTypes,
|
|
11842
12604
|
Object.keys(entityLookups),
|
|
11843
12605
|
t2("financial.itemType.other"),
|
|
11844
12606
|
direction
|
|
11845
12607
|
), [translatedItemTypes, entityLookups, direction, t2]);
|
|
11846
12608
|
const itemTypes = useStockedItemTypes(offeredItemTypes, entityLookups);
|
|
11847
|
-
const selectableAccounts =
|
|
12609
|
+
const selectableAccounts = React29__default.useMemo(() => {
|
|
11848
12610
|
const postable = postableAccounts(accounts);
|
|
11849
12611
|
if (!counterpartyAccounting.limitAccounts) return postable;
|
|
11850
12612
|
const allowed = new Set(counterpartyAccounting.allowedAccountIds);
|
|
11851
12613
|
return postable.filter((account) => allowed.has(account.id));
|
|
11852
12614
|
}, [accounts, counterpartyAccounting]);
|
|
11853
12615
|
const hasItemLookups = itemTypes.some((item) => entityLookups[item.value]);
|
|
11854
|
-
const amount =
|
|
12616
|
+
const amount = React29__default.useMemo(() => items.reduce(
|
|
11855
12617
|
(sum, item) => sum + item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
|
|
11856
12618
|
0
|
|
11857
12619
|
), [items]);
|
|
@@ -11859,8 +12621,8 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
|
|
|
11859
12621
|
const numericDueDay = Math.trunc(Number(dueDay));
|
|
11860
12622
|
const invalid = items.length === 0 ? "Adicione pelo menos um item." : items.some((item) => !item.description.trim()) ? "Informe a descri\xE7\xE3o de todos os itens." : !contact?.id ? `Selecione ${isPayable ? "quem recebe" : "quem paga"}.` : !(amount > 0) ? "Informe um valor maior que zero." : numericDueDay < 1 || numericDueDay > 31 ? "O dia do vencimento vai de 1 a 31." : !startsOn ? "Informe o in\xEDcio da recorr\xEAncia." : endsOn && endsOn < startsOn ? "O t\xE9rmino n\xE3o pode vir antes do in\xEDcio." : null;
|
|
11861
12623
|
const fields = { items, contactId: contact?.id ?? "", frequency, dueDay, startsOn, endsOn, unitId, notes, isActive };
|
|
11862
|
-
const snapshot =
|
|
11863
|
-
|
|
12624
|
+
const snapshot = React29__default.useRef(null);
|
|
12625
|
+
React29__default.useEffect(() => {
|
|
11864
12626
|
if (loaded && snapshot.current === null) snapshot.current = JSON.stringify(fields);
|
|
11865
12627
|
}, [loaded]);
|
|
11866
12628
|
const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
|
|
@@ -12121,7 +12883,9 @@ function Field2({ label, children }) {
|
|
|
12121
12883
|
] });
|
|
12122
12884
|
}
|
|
12123
12885
|
var FINANCIAL_MODULE_DEFAULTS = {
|
|
12124
|
-
commissions: true
|
|
12886
|
+
commissions: true,
|
|
12887
|
+
cashRegisters: true,
|
|
12888
|
+
closing: true
|
|
12125
12889
|
};
|
|
12126
12890
|
var FINANCIAL_DEFAULTS = {
|
|
12127
12891
|
autoInstallments: true,
|
|
@@ -12137,15 +12901,27 @@ function FinancialGeneralSettings({ config }) {
|
|
|
12137
12901
|
const s = useTenantPluginSettings("financial", FINANCIAL_DEFAULTS);
|
|
12138
12902
|
const mods = useTenantPluginSettings("financial", FINANCIAL_MODULE_DEFAULTS);
|
|
12139
12903
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
12140
|
-
config?.modules.commissions && /* @__PURE__ */
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12904
|
+
(config?.modules.commissions || config?.modules.cashRegisters) && /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("financial.settings.modules"), description: t2("financial.settings.modulesDesc"), children: [
|
|
12905
|
+
config?.modules.commissions && /* @__PURE__ */ jsx(
|
|
12906
|
+
ToggleRow,
|
|
12907
|
+
{
|
|
12908
|
+
label: t2("financial.settings.commissions"),
|
|
12909
|
+
description: t2("financial.settings.commissionsDesc"),
|
|
12910
|
+
checked: mods.get("commissions"),
|
|
12911
|
+
onChange: (v) => mods.set("commissions", v)
|
|
12912
|
+
}
|
|
12913
|
+
),
|
|
12914
|
+
config?.modules.cashRegisters && /* @__PURE__ */ jsx(
|
|
12915
|
+
ToggleRow,
|
|
12916
|
+
{
|
|
12917
|
+
label: t2("financial.nav.cashRegisters"),
|
|
12918
|
+
description: t2("financial.settings.cashRegistersDesc"),
|
|
12919
|
+
checked: mods.get("cashRegisters"),
|
|
12920
|
+
onChange: (v) => mods.set("cashRegisters", v)
|
|
12921
|
+
}
|
|
12922
|
+
)
|
|
12923
|
+
] }),
|
|
12924
|
+
/* @__PURE__ */ jsx(SettingsGroup, { title: t2("financial.settings.closing"), description: t2("financial.settings.closingDesc"), children: /* @__PURE__ */ jsx(ToggleRow, { label: t2("financial.settings.closing"), checked: mods.get("closing"), onChange: (v) => mods.set("closing", v) }) }),
|
|
12149
12925
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("financial.settings.invoices"), description: t2("financial.settings.invoicesDesc"), children: [
|
|
12150
12926
|
/* @__PURE__ */ jsx(ToggleRow, { label: t2("financial.settings.autoInstallments"), description: t2("financial.settings.autoInstallmentsDesc"), checked: s.get("autoInstallments"), onChange: (v) => s.set("autoInstallments", v) }),
|
|
12151
12927
|
/* @__PURE__ */ jsx(ToggleRow, { label: t2("financial.settings.requireDoc"), description: t2("financial.settings.requireDocDesc"), checked: s.get("requireDoc"), onChange: (v) => s.set("requireDoc", v) }),
|
|
@@ -12228,7 +13004,7 @@ function parseIntent(activeView) {
|
|
|
12228
13004
|
}
|
|
12229
13005
|
return { view: activeView };
|
|
12230
13006
|
}
|
|
12231
|
-
function buildNav(config, activeView, navigate, t2) {
|
|
13007
|
+
function buildNav(config, activeView, navigate, t2, closingOn = true) {
|
|
12232
13008
|
const items = [
|
|
12233
13009
|
{
|
|
12234
13010
|
id: "summary",
|
|
@@ -12277,7 +13053,7 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
12277
13053
|
]
|
|
12278
13054
|
});
|
|
12279
13055
|
}
|
|
12280
|
-
items.push({
|
|
13056
|
+
if (closingOn) items.push({
|
|
12281
13057
|
id: "closing",
|
|
12282
13058
|
label: "Fechamento",
|
|
12283
13059
|
icon: "CalendarCheck",
|
|
@@ -12371,9 +13147,17 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
12371
13147
|
function FinancialPage({ config: appConfig, provider, store: store6, registries }) {
|
|
12372
13148
|
const houseModules = useTenantPluginSettings("financial", FINANCIAL_MODULE_DEFAULTS);
|
|
12373
13149
|
const commissionsOn = houseModules.get("commissions");
|
|
13150
|
+
const cashOn = houseModules.get("cashRegisters");
|
|
12374
13151
|
const config = useMemo(
|
|
12375
|
-
() =>
|
|
12376
|
-
|
|
13152
|
+
() => ({
|
|
13153
|
+
...appConfig,
|
|
13154
|
+
modules: {
|
|
13155
|
+
...appConfig.modules,
|
|
13156
|
+
commissions: appConfig.modules.commissions && commissionsOn,
|
|
13157
|
+
cashRegisters: appConfig.modules.cashRegisters && cashOn
|
|
13158
|
+
}
|
|
13159
|
+
}),
|
|
13160
|
+
[appConfig, commissionsOn, cashOn]
|
|
12377
13161
|
);
|
|
12378
13162
|
const { view, direction, navigate, previousView, back } = useModuleNavigation("/financial", {
|
|
12379
13163
|
summary: 0,
|
|
@@ -12411,7 +13195,7 @@ function FinancialPage({ config: appConfig, provider, store: store6, registries
|
|
|
12411
13195
|
const intent = parseIntent(view);
|
|
12412
13196
|
const isSettings = view === "settings";
|
|
12413
13197
|
const isSummary = intent.view === "summary";
|
|
12414
|
-
const nav = useModuleNavAccess(buildNav(config, view, navigate, t2));
|
|
13198
|
+
const nav = useModuleNavAccess(buildNav(config, view, navigate, t2, houseModules.get("closing")));
|
|
12415
13199
|
const quickActions = useMemo(() => {
|
|
12416
13200
|
if (!canCreate) return [];
|
|
12417
13201
|
const actions = [];
|
|
@@ -12557,7 +13341,9 @@ function TotalBalanceKpi() {
|
|
|
12557
13341
|
const summary = useFinancialStore((s) => s.summary);
|
|
12558
13342
|
const loading = useFinancialStore((s) => s.summaryLoading);
|
|
12559
13343
|
useEnsureSummary();
|
|
12560
|
-
|
|
13344
|
+
const left = (summary?.excluded ?? []).map((x) => formatCurrency(x.balance || x.receivable - x.payable || x.inflow - x.outflow, resolveCurrency(currency, x.currency))).join(" \xB7 ");
|
|
13345
|
+
const sub = left ? `Fora do total, sem c\xE2mbio dispon\xEDvel: ${left}` : t2("financial.summary.allAccounts");
|
|
13346
|
+
return /* @__PURE__ */ jsx(KpiCard, { label: t2("financial.summary.totalBalance"), icon: "Wallet", value: formatCurrency(summary?.totalBalance ?? 0, currency), sub: loading ? void 0 : sub });
|
|
12561
13347
|
}
|
|
12562
13348
|
function ReceivableKpi() {
|
|
12563
13349
|
const t2 = useTranslation();
|
|
@@ -12707,7 +13493,7 @@ function BalanceHero() {
|
|
|
12707
13493
|
const summary = useFinancialStore((s) => s.summary);
|
|
12708
13494
|
const navigate = useDashboardNavigate();
|
|
12709
13495
|
useEnsureSummary();
|
|
12710
|
-
const [hidden, setHidden] =
|
|
13496
|
+
const [hidden, setHidden] = React29__default.useState(false);
|
|
12711
13497
|
const money4 = (v) => hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022" : formatCurrency(v ?? 0, currency);
|
|
12712
13498
|
const inflow = summary?.monthlyInflow ?? 0;
|
|
12713
13499
|
const outflow = summary?.monthlyOutflow ?? 0;
|
|
@@ -12780,7 +13566,7 @@ function categorizeSpend(notes) {
|
|
|
12780
13566
|
function SpendByCategoryChart() {
|
|
12781
13567
|
const t2 = useTranslation();
|
|
12782
13568
|
const provider = useFinancialProvider();
|
|
12783
|
-
const [rows, setRows] =
|
|
13569
|
+
const [rows, setRows] = React29__default.useState([]);
|
|
12784
13570
|
useEffect(() => {
|
|
12785
13571
|
let alive = true;
|
|
12786
13572
|
void (async () => {
|
|
@@ -12823,7 +13609,7 @@ function UpcomingBills() {
|
|
|
12823
13609
|
const { currency } = useFinancialConfig();
|
|
12824
13610
|
const provider = useFinancialProvider();
|
|
12825
13611
|
const dueLabel = useDueLabel();
|
|
12826
|
-
const [bills, setBills] =
|
|
13612
|
+
const [bills, setBills] = React29__default.useState([]);
|
|
12827
13613
|
useEffect(() => {
|
|
12828
13614
|
let alive = true;
|
|
12829
13615
|
void (async () => {
|
|
@@ -14062,6 +14848,34 @@ function createMockFinancialProvider(options) {
|
|
|
14062
14848
|
return provider;
|
|
14063
14849
|
}
|
|
14064
14850
|
|
|
14851
|
+
// src/lib/fx-auto.ts
|
|
14852
|
+
var ENDPOINT = "https://api.frankfurter.dev/v1/latest";
|
|
14853
|
+
var SOURCE = "BCE \xB7 refer\xEAncia do dia (autom\xE1tico)";
|
|
14854
|
+
var attempted = /* @__PURE__ */ new Set();
|
|
14855
|
+
async function ensureReferenceRates(pub, tenantId, house, currencies) {
|
|
14856
|
+
const wanted = [...new Set(currencies.map((c) => c.toUpperCase()))].filter((c) => c !== house && !attempted.has(`${tenantId}:${house}:${c}`));
|
|
14857
|
+
if (wanted.length === 0 || typeof fetch !== "function") return false;
|
|
14858
|
+
wanted.forEach((c) => attempted.add(`${tenantId}:${house}:${c}`));
|
|
14859
|
+
try {
|
|
14860
|
+
const res = await fetch(`${ENDPOINT}?base=${encodeURIComponent(house)}&symbols=${wanted.map(encodeURIComponent).join(",")}`);
|
|
14861
|
+
if (!res.ok) return false;
|
|
14862
|
+
const body = await res.json();
|
|
14863
|
+
const rows = Object.entries(body.rates ?? {}).filter(([, rate]) => typeof rate === "number" && rate > 0).map(([quote, rate]) => ({
|
|
14864
|
+
tenant_id: tenantId,
|
|
14865
|
+
rate_date: body.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
14866
|
+
base_currency: house,
|
|
14867
|
+
quote_currency: quote,
|
|
14868
|
+
rate,
|
|
14869
|
+
source: SOURCE
|
|
14870
|
+
}));
|
|
14871
|
+
if (rows.length === 0) return false;
|
|
14872
|
+
const { error } = await pub.from("plg_financial_fx_rates").upsert(rows, { onConflict: "tenant_id,base_currency,quote_currency,rate_date", ignoreDuplicates: true });
|
|
14873
|
+
return !error;
|
|
14874
|
+
} catch {
|
|
14875
|
+
return false;
|
|
14876
|
+
}
|
|
14877
|
+
}
|
|
14878
|
+
|
|
14065
14879
|
// src/data/supabase.ts
|
|
14066
14880
|
function hasCriteriaValue(v) {
|
|
14067
14881
|
if (!v) return false;
|
|
@@ -14253,6 +15067,24 @@ function billRowToMovement(r) {
|
|
|
14253
15067
|
updatedAt: r.updated_at
|
|
14254
15068
|
};
|
|
14255
15069
|
}
|
|
15070
|
+
async function attachCurrency(pub, invoices) {
|
|
15071
|
+
const ids = [...new Set(invoices.map((inv) => inv.invoiceId ?? inv.id).filter(Boolean))];
|
|
15072
|
+
if (ids.length === 0) return;
|
|
15073
|
+
try {
|
|
15074
|
+
const { data, error } = await pub.from("plg_financial_invoices").select("id, currency").in("id", ids);
|
|
15075
|
+
if (error || !Array.isArray(data)) return;
|
|
15076
|
+
const byId = new Map(data.map((r) => [r.id, r.currency]));
|
|
15077
|
+
for (const inv of invoices) inv.currency = byId.get(inv.invoiceId ?? inv.id) ?? inv.currency;
|
|
15078
|
+
} catch {
|
|
15079
|
+
}
|
|
15080
|
+
}
|
|
15081
|
+
async function attachTags(pub, invoice) {
|
|
15082
|
+
try {
|
|
15083
|
+
const { data, error } = await pub.from("plg_financial_invoices").select("tags").eq("id", invoice.id).maybeSingle();
|
|
15084
|
+
if (!error && Array.isArray(data?.tags)) invoice.tags = data.tags;
|
|
15085
|
+
} catch {
|
|
15086
|
+
}
|
|
15087
|
+
}
|
|
14256
15088
|
function rowToInvoiceLine(r) {
|
|
14257
15089
|
return {
|
|
14258
15090
|
id: r.installment_id,
|
|
@@ -14536,25 +15368,47 @@ function createSupabaseFinancialProvider() {
|
|
|
14536
15368
|
l.attachmentCount = porTitulo.get(l.invoiceId ?? l.id) ?? 0;
|
|
14537
15369
|
}
|
|
14538
15370
|
}
|
|
15371
|
+
await attachCurrency(pub, linhas);
|
|
14539
15372
|
return { data: linhas, total: count ?? 0 };
|
|
14540
15373
|
},
|
|
14541
15374
|
async getInvoiceById(id) {
|
|
14542
15375
|
const { pub } = getClients();
|
|
14543
15376
|
const { data, error } = await pub.from("v_financial_invoice_balances").select("*").eq("id", id).maybeSingle();
|
|
14544
15377
|
if (error) throw error;
|
|
14545
|
-
|
|
15378
|
+
if (!data) return null;
|
|
15379
|
+
const invoice = rowToInvoice(data);
|
|
15380
|
+
await Promise.all([attachCurrency(pub, [invoice]), attachTags(pub, invoice)]);
|
|
15381
|
+
return invoice;
|
|
14546
15382
|
},
|
|
14547
15383
|
async getInvoiceItems(invoiceId) {
|
|
14548
15384
|
const { pub } = getClients();
|
|
14549
15385
|
const { data, error } = await pub.from("v_financial_invoice_lines").select("*").eq("invoice_id", invoiceId);
|
|
14550
15386
|
if (error) throw error;
|
|
14551
15387
|
const rows = data ?? [];
|
|
15388
|
+
const metaOf = (r) => r.item_metadata ?? r.metadata ?? {};
|
|
15389
|
+
const productOf = (r) => r.product_id ?? (metaOf(r).itemKind === "product" ? metaOf(r).referenceId : void 0);
|
|
15390
|
+
const productIds = [...new Set(rows.map(productOf).filter(Boolean))];
|
|
15391
|
+
const pictures = /* @__PURE__ */ new Map();
|
|
15392
|
+
if (productIds.length > 0) {
|
|
15393
|
+
try {
|
|
15394
|
+
const found = await pub.from("products").select("id, image_url").in("id", productIds);
|
|
15395
|
+
for (const p of Array.isArray(found.data) ? found.data : []) {
|
|
15396
|
+
if (p.image_url) pictures.set(p.id, p.image_url);
|
|
15397
|
+
}
|
|
15398
|
+
} catch {
|
|
15399
|
+
}
|
|
15400
|
+
}
|
|
14552
15401
|
return rows.map((r) => {
|
|
14553
15402
|
const i = snakeToCamel(r);
|
|
15403
|
+
const meta = metaOf(r);
|
|
15404
|
+
const pictured = productOf(r);
|
|
14554
15405
|
return {
|
|
15406
|
+
imageUrl: pictured ? pictures.get(pictured) : void 0,
|
|
15407
|
+
// A line that names a product IS a product line, whatever it was
|
|
15408
|
+
// typed as: linking it later (stock entry) must change its icon too.
|
|
14555
15409
|
id: i.itemId,
|
|
14556
15410
|
invoiceId,
|
|
14557
|
-
itemKind:
|
|
15411
|
+
itemKind: meta.itemKind && meta.itemKind !== "other" ? meta.itemKind : i.productId ? "product" : "other",
|
|
14558
15412
|
// `name` OU `description`: quem levanta a conta pelo SQL
|
|
14559
15413
|
// (`financial_create_payable`, o borderô de comissões) escreve o
|
|
14560
15414
|
// texto em `description` e deixa `name` nulo. Lendo só `name`, a
|
|
@@ -14571,12 +15425,12 @@ function createSupabaseFinancialProvider() {
|
|
|
14571
15425
|
// escolha de catálogo é gravada. Lendo só a coluna, reabrir uma
|
|
14572
15426
|
// fatura apagava o item escolhido de toda linha e deixava o texto
|
|
14573
15427
|
// solto — e salvar de novo confirmava a perda.
|
|
14574
|
-
referenceId: i.productId ??
|
|
15428
|
+
referenceId: i.productId ?? meta.referenceId,
|
|
14575
15429
|
accountId: i.chartAccountId,
|
|
14576
15430
|
costCenterId: i.costCenterId,
|
|
14577
15431
|
// De onde a linha veio — o pacote que a gerou, por exemplo. Sem
|
|
14578
15432
|
// devolvê-lo, editar a fatura desfazia o vínculo em silêncio.
|
|
14579
|
-
metadata:
|
|
15433
|
+
metadata: r.item_metadata ?? r.metadata ?? void 0,
|
|
14580
15434
|
cancelledAt: i.itemCancelledAt ?? void 0,
|
|
14581
15435
|
cancelReason: i.itemCancelReason ?? void 0,
|
|
14582
15436
|
createdAt: i.createdAt
|
|
@@ -14671,11 +15525,18 @@ function createSupabaseFinancialProvider() {
|
|
|
14671
15525
|
p_competence_date: input.invoiceDate ?? null,
|
|
14672
15526
|
p_description: itemsSummary || null,
|
|
14673
15527
|
p_notes: input.observations ?? null,
|
|
14674
|
-
p_metadata: {
|
|
15528
|
+
p_metadata: {
|
|
15529
|
+
contactName: input.contactName,
|
|
15530
|
+
...input.fiscalNumber ? { fiscalNumber: input.fiscalNumber } : {},
|
|
15531
|
+
...input.currency ? { currency: input.currency } : {}
|
|
15532
|
+
},
|
|
14675
15533
|
p_idempotency_key: genId()
|
|
14676
15534
|
});
|
|
14677
15535
|
if (error) throw error;
|
|
14678
15536
|
const invoiceId = res.invoice_id;
|
|
15537
|
+
if (input.tags?.length) {
|
|
15538
|
+
await pub.from("plg_financial_invoices").update({ tags: input.tags }).eq("id", invoiceId);
|
|
15539
|
+
}
|
|
14679
15540
|
if (input.items.length > 0) {
|
|
14680
15541
|
const { data: inserted } = await core.from("items").insert(
|
|
14681
15542
|
input.items.map((item, i) => ({
|
|
@@ -14709,6 +15570,7 @@ function createSupabaseFinancialProvider() {
|
|
|
14709
15570
|
const { pub } = getClients();
|
|
14710
15571
|
const row = {};
|
|
14711
15572
|
if (partial.observations !== void 0) row.notes = partial.observations;
|
|
15573
|
+
if (partial.tags !== void 0) row.tags = partial.tags;
|
|
14712
15574
|
if (partial.invoiceDate !== void 0) row.competence_date = partial.invoiceDate;
|
|
14713
15575
|
if (partial.contactId !== void 0) row.counterparty_person_id = partial.contactId ?? null;
|
|
14714
15576
|
if (partial.unitId !== void 0) row.unit_id = partial.unitId ?? null;
|
|
@@ -14828,17 +15690,110 @@ function createSupabaseFinancialProvider() {
|
|
|
14828
15690
|
},
|
|
14829
15691
|
async getInvoiceStockEntries(invoiceId) {
|
|
14830
15692
|
const { pub } = getClients();
|
|
14831
|
-
const
|
|
14832
|
-
if (error) throw error;
|
|
14833
|
-
return (data ?? []).map((r) => ({
|
|
15693
|
+
const toEntry = (r) => ({
|
|
14834
15694
|
itemId: r.item_id,
|
|
14835
15695
|
productId: r.product_id,
|
|
14836
15696
|
locationId: r.location_id,
|
|
14837
15697
|
entered: Number(r.entered ?? 0),
|
|
14838
15698
|
available: Number(r.available ?? 0),
|
|
14839
15699
|
enteredOn: r.entered_on
|
|
15700
|
+
});
|
|
15701
|
+
const { data, error } = await pub.rpc("financial_invoice_stock_entries", { p_invoice_id: invoiceId });
|
|
15702
|
+
if (error) throw error;
|
|
15703
|
+
const guessed = (data ?? []).map(toEntry);
|
|
15704
|
+
const linked = await pub.rpc("financial_invoice_stock_links", { p_invoice_id: invoiceId });
|
|
15705
|
+
const direct = linked.error || !Array.isArray(linked.data) ? [] : linked.data.map(toEntry);
|
|
15706
|
+
const seen = new Set(direct.map((e) => e.itemId));
|
|
15707
|
+
return [...direct, ...guessed.filter((e) => !seen.has(e.itemId))];
|
|
15708
|
+
},
|
|
15709
|
+
async listStockLocations() {
|
|
15710
|
+
const { pub } = getClients();
|
|
15711
|
+
const { data, error } = await pub.from("plg_inventory_stock_locations").select("id, name").eq("is_active", true).order("name");
|
|
15712
|
+
if (error) return [];
|
|
15713
|
+
return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
|
|
15714
|
+
},
|
|
15715
|
+
async listCurrencies() {
|
|
15716
|
+
const { pub } = getClients();
|
|
15717
|
+
const { data, error } = await pub.from("plg_financial_accounts").select("currency").eq("tenant_id", getTenantId()).eq("is_active", true).not("kind", "in", "(system,person)");
|
|
15718
|
+
if (error) return [];
|
|
15719
|
+
return [...new Set((Array.isArray(data) ? data : []).map((r) => String(r.currency ?? "").toUpperCase()).filter(Boolean))];
|
|
15720
|
+
},
|
|
15721
|
+
async getProductPictures(ids) {
|
|
15722
|
+
if (ids.length === 0) return {};
|
|
15723
|
+
const { pub } = getClients();
|
|
15724
|
+
const { data, error } = await pub.from("products").select("id, image_url").in("id", ids);
|
|
15725
|
+
if (error) return {};
|
|
15726
|
+
const out = {};
|
|
15727
|
+
for (const p of Array.isArray(data) ? data : []) if (p.image_url) out[p.id] = p.image_url;
|
|
15728
|
+
return out;
|
|
15729
|
+
},
|
|
15730
|
+
async searchStockProducts(query) {
|
|
15731
|
+
const { pub } = getClients();
|
|
15732
|
+
let qb = pub.from("products").select("id, name").eq("kind", "good").eq("is_active", true).order("name").limit(20);
|
|
15733
|
+
const q = query.trim();
|
|
15734
|
+
if (q) qb = qb.ilike("name", `%${q.replace(/[%_]/g, " ")}%`);
|
|
15735
|
+
const { data, error } = await qb;
|
|
15736
|
+
if (error) return [];
|
|
15737
|
+
return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
|
|
15738
|
+
},
|
|
15739
|
+
async getFxRate(from, to) {
|
|
15740
|
+
const { pub } = getClients();
|
|
15741
|
+
if (from === to) return 1;
|
|
15742
|
+
const read = async () => {
|
|
15743
|
+
const { data, error } = await pub.from("plg_financial_fx_rates").select("base_currency, quote_currency, rate, rate_date").or(`and(base_currency.eq.${from},quote_currency.eq.${to}),and(base_currency.eq.${to},quote_currency.eq.${from})`).order("rate_date", { ascending: false }).limit(1);
|
|
15744
|
+
const row = !error && Array.isArray(data) ? data[0] : null;
|
|
15745
|
+
if (!row || !(Number(row.rate) > 0)) return null;
|
|
15746
|
+
return row.base_currency === from ? Number(row.rate) : 1 / Number(row.rate);
|
|
15747
|
+
};
|
|
15748
|
+
const known = await read();
|
|
15749
|
+
if (known !== null) return known;
|
|
15750
|
+
const tenantId = getTenantId();
|
|
15751
|
+
if (tenantId && await ensureReferenceRates(pub, tenantId, to, [from])) return read();
|
|
15752
|
+
return null;
|
|
15753
|
+
},
|
|
15754
|
+
async listProductVariants(productId) {
|
|
15755
|
+
const { pub } = getClients();
|
|
15756
|
+
const { data, error } = await pub.from("product_variants").select("id, name, is_default").eq("product_id", productId).eq("is_active", true).order("sort_order").order("created_at");
|
|
15757
|
+
if (error) return [];
|
|
15758
|
+
return (data ?? []).map((r) => ({ id: r.id, name: r.name, isDefault: !!r.is_default }));
|
|
15759
|
+
},
|
|
15760
|
+
async getInvoiceStockDetails(invoiceId) {
|
|
15761
|
+
const { pub } = getClients();
|
|
15762
|
+
const { data, error } = await pub.rpc("financial_invoice_stock_link_details", { p_invoice_id: invoiceId });
|
|
15763
|
+
if (error || !Array.isArray(data)) return [];
|
|
15764
|
+
return data.map((r) => ({
|
|
15765
|
+
itemId: r.item_id,
|
|
15766
|
+
variantId: r.variant_id ?? void 0,
|
|
15767
|
+
variantName: r.variant_name ?? void 0,
|
|
15768
|
+
quantity: Number(r.quantity) || 0,
|
|
15769
|
+
unitCost: Number(r.unit_cost) || 0,
|
|
15770
|
+
currency: String(r.currency ?? ""),
|
|
15771
|
+
sourceUnitCost: r.source_unit_cost == null ? void 0 : Number(r.source_unit_cost),
|
|
15772
|
+
sourceCurrency: r.source_currency ?? void 0,
|
|
15773
|
+
fxRate: r.fx_rate == null ? void 0 : Number(r.fx_rate),
|
|
15774
|
+
locationName: r.location_name ?? void 0,
|
|
15775
|
+
enteredOn: r.entered_on ?? void 0
|
|
14840
15776
|
}));
|
|
14841
15777
|
},
|
|
15778
|
+
async receiveInvoiceItemStock(input) {
|
|
15779
|
+
const { pub } = getClients();
|
|
15780
|
+
const call = () => pub.rpc("financial_invoice_item_receive_stock_lines", {
|
|
15781
|
+
p_item_id: input.itemId,
|
|
15782
|
+
p_location_id: input.locationId,
|
|
15783
|
+
p_lines: input.entries.map((e) => ({ variant_id: e.variantId ?? null, quantity: e.quantity })),
|
|
15784
|
+
p_product_id: input.productId ?? null,
|
|
15785
|
+
p_unit_cost: input.unitCost ?? null,
|
|
15786
|
+
p_fx_rate: input.fxRate ?? null,
|
|
15787
|
+
p_idempotency_key: input.idempotencyKey ?? null
|
|
15788
|
+
});
|
|
15789
|
+
let { error } = await call();
|
|
15790
|
+
const pair = /de ([A-Z]{3}) para ([A-Z]{3})/.exec(String(error?.message ?? ""));
|
|
15791
|
+
const tenantId = getTenantId();
|
|
15792
|
+
if (pair && tenantId && await ensureReferenceRates(pub, tenantId, pair[2], [pair[1]])) {
|
|
15793
|
+
({ error } = await call());
|
|
15794
|
+
}
|
|
15795
|
+
if (error) throw error;
|
|
15796
|
+
},
|
|
14842
15797
|
async cancelInvoiceItems(input) {
|
|
14843
15798
|
const { pub } = getClients();
|
|
14844
15799
|
const { data, error } = await pub.rpc("financial_cancel_invoice_items", {
|
|
@@ -16147,13 +17102,72 @@ function createSupabaseFinancialProvider() {
|
|
|
16147
17102
|
},
|
|
16148
17103
|
async getSummary(dateRange) {
|
|
16149
17104
|
const { pub } = getClients();
|
|
16150
|
-
const
|
|
16151
|
-
|
|
16152
|
-
|
|
16153
|
-
|
|
17105
|
+
const n = (v) => Number(v) || 0;
|
|
17106
|
+
const args = { p_from: dateRange?.from ?? null, p_to: dateRange?.to ?? null };
|
|
17107
|
+
let split = await pub.rpc("financial_summary_by_currency", args);
|
|
17108
|
+
if (!split.error && Array.isArray(split.data)) {
|
|
17109
|
+
const missing = split.data.filter((r) => r.fx_rate === null || r.fx_rate === void 0);
|
|
17110
|
+
if (missing.length > 0) {
|
|
17111
|
+
const house = String(missing[0].house_currency);
|
|
17112
|
+
const tenantId = getTenantId();
|
|
17113
|
+
if (tenantId && await ensureReferenceRates(pub, tenantId, house, missing.map((r) => String(r.currency)))) {
|
|
17114
|
+
const again = await pub.rpc("financial_summary_by_currency", args);
|
|
17115
|
+
if (!again.error && Array.isArray(again.data)) split = again;
|
|
17116
|
+
}
|
|
17117
|
+
}
|
|
17118
|
+
const total = {
|
|
17119
|
+
totalBalance: 0,
|
|
17120
|
+
totalReceivable: 0,
|
|
17121
|
+
totalPayable: 0,
|
|
17122
|
+
monthlyInflow: 0,
|
|
17123
|
+
monthlyOutflow: 0,
|
|
17124
|
+
overdueReceivableCount: 0,
|
|
17125
|
+
overdueReceivableAmount: 0,
|
|
17126
|
+
overduePayableCount: 0,
|
|
17127
|
+
overduePayableAmount: 0
|
|
17128
|
+
};
|
|
17129
|
+
const excluded = [];
|
|
17130
|
+
for (const r of split.data) {
|
|
17131
|
+
total.overdueReceivableCount += n(r.overdue_receivable_count);
|
|
17132
|
+
total.overduePayableCount += n(r.overdue_payable_count);
|
|
17133
|
+
if (r.fx_rate === null || r.fx_rate === void 0) {
|
|
17134
|
+
const left = {
|
|
17135
|
+
currency: String(r.currency),
|
|
17136
|
+
balance: n(r.total_balance),
|
|
17137
|
+
receivable: n(r.total_receivable),
|
|
17138
|
+
payable: n(r.total_payable),
|
|
17139
|
+
inflow: n(r.monthly_inflow),
|
|
17140
|
+
outflow: n(r.monthly_outflow)
|
|
17141
|
+
};
|
|
17142
|
+
if (left.balance || left.receivable || left.payable || left.inflow || left.outflow) excluded.push(left);
|
|
17143
|
+
continue;
|
|
17144
|
+
}
|
|
17145
|
+
const rate = n(r.fx_rate);
|
|
17146
|
+
total.totalBalance += n(r.total_balance) * rate;
|
|
17147
|
+
total.totalReceivable += n(r.total_receivable) * rate;
|
|
17148
|
+
total.totalPayable += n(r.total_payable) * rate;
|
|
17149
|
+
total.monthlyInflow += n(r.monthly_inflow) * rate;
|
|
17150
|
+
total.monthlyOutflow += n(r.monthly_outflow) * rate;
|
|
17151
|
+
total.overdueReceivableAmount += n(r.overdue_receivable_amount) * rate;
|
|
17152
|
+
total.overduePayableAmount += n(r.overdue_payable_amount) * rate;
|
|
17153
|
+
}
|
|
17154
|
+
const cents2 = (v) => Math.round(v * 100) / 100;
|
|
17155
|
+
return {
|
|
17156
|
+
totalBalance: cents2(total.totalBalance),
|
|
17157
|
+
totalReceivable: cents2(total.totalReceivable),
|
|
17158
|
+
totalPayable: cents2(total.totalPayable),
|
|
17159
|
+
monthlyInflow: cents2(total.monthlyInflow),
|
|
17160
|
+
monthlyOutflow: cents2(total.monthlyOutflow),
|
|
17161
|
+
overdueReceivableCount: total.overdueReceivableCount,
|
|
17162
|
+
overdueReceivableAmount: cents2(total.overdueReceivableAmount),
|
|
17163
|
+
overduePayableCount: total.overduePayableCount,
|
|
17164
|
+
overduePayableAmount: cents2(total.overduePayableAmount),
|
|
17165
|
+
...excluded.length ? { excluded } : {}
|
|
17166
|
+
};
|
|
17167
|
+
}
|
|
17168
|
+
const { data, error } = await pub.rpc("financial_summary", args);
|
|
16154
17169
|
if (error) throw error;
|
|
16155
17170
|
const row = (Array.isArray(data) ? data[0] : data) ?? {};
|
|
16156
|
-
const n = (v) => Number(v) || 0;
|
|
16157
17171
|
return {
|
|
16158
17172
|
totalBalance: n(row.total_balance),
|
|
16159
17173
|
totalReceivable: n(row.total_receivable),
|
|
@@ -17228,21 +18242,21 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
|
|
|
17228
18242
|
const canCreate = can("financial", "create");
|
|
17229
18243
|
const canEdit = can("financial", "edit");
|
|
17230
18244
|
const canDelete = can("financial", "delete");
|
|
17231
|
-
const provider =
|
|
18245
|
+
const provider = React29__default.useMemo(
|
|
17232
18246
|
() => dataProvider ?? resolveDataProvider(entity, mockData ?? []),
|
|
17233
18247
|
[entity, mockData, dataProvider]
|
|
17234
18248
|
);
|
|
17235
18249
|
const table = entity.data?.table;
|
|
17236
|
-
const [nodes, setNodes] =
|
|
17237
|
-
const [loadError, setLoadError] =
|
|
17238
|
-
const [expanded, setExpanded] =
|
|
17239
|
-
const [query, setQuery] =
|
|
17240
|
-
const [showInactive, setShowInactive] =
|
|
17241
|
-
const [editing, setEditing] =
|
|
17242
|
-
const [confirmDelete, setConfirmDelete] =
|
|
17243
|
-
const [busy, setBusy] =
|
|
17244
|
-
const initialized =
|
|
17245
|
-
const reload =
|
|
18250
|
+
const [nodes, setNodes] = React29__default.useState(null);
|
|
18251
|
+
const [loadError, setLoadError] = React29__default.useState(null);
|
|
18252
|
+
const [expanded, setExpanded] = React29__default.useState(/* @__PURE__ */ new Set());
|
|
18253
|
+
const [query, setQuery] = React29__default.useState("");
|
|
18254
|
+
const [showInactive, setShowInactive] = React29__default.useState(true);
|
|
18255
|
+
const [editing, setEditing] = React29__default.useState(null);
|
|
18256
|
+
const [confirmDelete, setConfirmDelete] = React29__default.useState(null);
|
|
18257
|
+
const [busy, setBusy] = React29__default.useState(false);
|
|
18258
|
+
const initialized = React29__default.useRef(false);
|
|
18259
|
+
const reload = React29__default.useCallback(async () => {
|
|
17246
18260
|
try {
|
|
17247
18261
|
const rows = await loadAll(provider);
|
|
17248
18262
|
setNodes(rows);
|
|
@@ -17257,17 +18271,17 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
|
|
|
17257
18271
|
setNodes((prev) => prev ?? []);
|
|
17258
18272
|
}
|
|
17259
18273
|
}, [provider]);
|
|
17260
|
-
|
|
18274
|
+
React29__default.useEffect(() => {
|
|
17261
18275
|
void reload();
|
|
17262
18276
|
}, [reload]);
|
|
17263
|
-
const shown =
|
|
18277
|
+
const shown = React29__default.useMemo(
|
|
17264
18278
|
() => (nodes ?? []).filter((n) => showInactive || n.isActive),
|
|
17265
18279
|
[nodes, showInactive]
|
|
17266
18280
|
);
|
|
17267
|
-
const tree =
|
|
17268
|
-
const fullTree =
|
|
17269
|
-
const search =
|
|
17270
|
-
const groupIds =
|
|
18281
|
+
const tree = React29__default.useMemo(() => buildChartTree(shown), [shown]);
|
|
18282
|
+
const fullTree = React29__default.useMemo(() => buildChartTree(nodes ?? []), [nodes]);
|
|
18283
|
+
const search = React29__default.useMemo(() => searchVisibleIds(tree, query), [tree, query]);
|
|
18284
|
+
const groupIds = React29__default.useMemo(
|
|
17271
18285
|
() => [...tree.byId.values()].filter((n) => isGroupNode(tree, n)).map((n) => n.id),
|
|
17272
18286
|
[tree]
|
|
17273
18287
|
);
|
|
@@ -17363,7 +18377,7 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
|
|
|
17363
18377
|
const ok = await run(() => provider.remove(node.id), t2("financial.chartTree.deleted", { name: node.name }));
|
|
17364
18378
|
if (ok) setConfirmDelete(null);
|
|
17365
18379
|
}
|
|
17366
|
-
const parentOptions =
|
|
18380
|
+
const parentOptions = React29__default.useCallback((q, selfId) => {
|
|
17367
18381
|
const blocked = selfId ? descendantIds(fullTree, selfId).add(selfId) : /* @__PURE__ */ new Set();
|
|
17368
18382
|
const needle = q.trim().toLowerCase();
|
|
17369
18383
|
return [...fullTree.byId.values()].filter((n) => !blocked.has(n.id) && isGroupNode(fullTree, n)).filter((n) => !needle || accountLabel(n).toLowerCase().includes(needle)).sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true })).slice(0, 50).map((n) => ({ id: n.id, label: accountLabel(n) }));
|
|
@@ -17666,7 +18680,7 @@ var chartOfAccountsSeed = [
|
|
|
17666
18680
|
{ id: "coa-4.2", code: "4.2", name: "Utilities", nodeType: "leaf", accountType: "expense", parentId: "coa-4", isActive: true },
|
|
17667
18681
|
{ id: "coa-4.3", code: "4.3", name: "Payroll", nodeType: "leaf", accountType: "expense", parentId: "coa-4", isActive: true }
|
|
17668
18682
|
];
|
|
17669
|
-
var ChartOfAccountsRegistryView = (props) =>
|
|
18683
|
+
var ChartOfAccountsRegistryView = (props) => React29__default.createElement(ChartOfAccountsTree, { ...props, entity: chartOfAccountsEntity, mockData: chartOfAccountsSeed });
|
|
17670
18684
|
ChartOfAccountsRegistryView.displayName = "ChartOfAccountsRegistryView";
|
|
17671
18685
|
var costCenterEntity = {
|
|
17672
18686
|
name: "Cost Center",
|
|
@@ -17764,7 +18778,7 @@ var bankAccountEntity = {
|
|
|
17764
18778
|
labelWhen: (v) => v.kind === "card_payable" ? "Banco emissor" : void 0,
|
|
17765
18779
|
visibleWhen: (v) => hasIssuer(v.kind),
|
|
17766
18780
|
group: "bank",
|
|
17767
|
-
renderInput: ({ value, values, onChange, setValue }) =>
|
|
18781
|
+
renderInput: ({ value, values, onChange, setValue }) => React29__default.createElement(BankPicker, {
|
|
17768
18782
|
code: values.bankCode ?? "",
|
|
17769
18783
|
name: value ?? "",
|
|
17770
18784
|
onChange: (next) => {
|
|
@@ -18628,15 +19642,20 @@ function buildFinancialOnboarding() {
|
|
|
18628
19642
|
]
|
|
18629
19643
|
};
|
|
18630
19644
|
}
|
|
18631
|
-
function CashSessionHeaderControl(
|
|
19645
|
+
function CashSessionHeaderControl(props) {
|
|
19646
|
+
const house = useTenantPluginSettings("financial", FINANCIAL_MODULE_DEFAULTS);
|
|
19647
|
+
if (!house.get("cashRegisters")) return null;
|
|
19648
|
+
return /* @__PURE__ */ jsx(CashSessionControl, { ...props });
|
|
19649
|
+
}
|
|
19650
|
+
function CashSessionControl({ config, provider, store: store6 }) {
|
|
18632
19651
|
const sessions = useStore(store6, (s) => s.cashSessions);
|
|
18633
19652
|
const fetchCashSessions = useStore(store6, (s) => s.fetchCashSessions);
|
|
18634
19653
|
const open = useStore(store6, (s) => s.cashSessionModalOpen);
|
|
18635
19654
|
const setOpen = useStore(store6, (s) => s.setCashSessionModalOpen);
|
|
18636
|
-
|
|
19655
|
+
React29__default.useEffect(() => {
|
|
18637
19656
|
void fetchCashSessions?.();
|
|
18638
19657
|
}, [fetchCashSessions]);
|
|
18639
|
-
|
|
19658
|
+
React29__default.useEffect(() => {
|
|
18640
19659
|
if (!open) return;
|
|
18641
19660
|
const close = () => setOpen(false);
|
|
18642
19661
|
window.addEventListener("hashchange", close);
|
|
@@ -18798,6 +19817,7 @@ var en = {
|
|
|
18798
19817
|
"financial.invoice.reverseReason": "Reason for reversal",
|
|
18799
19818
|
"financial.invoice.paymentSchedule": "Payment Schedule",
|
|
18800
19819
|
"financial.invoiceForm.scheduleLocked": "Some installments are paid \u2014 the schedule can no longer be regenerated.",
|
|
19820
|
+
"financial.invoiceForm.scheduleHasCancelled": "This bill has a cancelled installment \u2014 amount and schedule can no longer be redone, only the due date.",
|
|
18801
19821
|
"financial.invoiceForm.issuedLocked": "This bill is issued: amount, items, supplier and date are locked. To change them, cancel it and create a new one. Notes, unit, document number and due dates can still be edited.",
|
|
18802
19822
|
"financial.invoiceForm.issuedEditable": "This bill is issued: amount, items and supplier are locked. To change them, cancel it and create a new one. While nothing has been paid, the date and the installment plan can still be edited.",
|
|
18803
19823
|
"financial.invoiceForm.edit": "Edit",
|
|
@@ -18812,6 +19832,8 @@ var en = {
|
|
|
18812
19832
|
"financial.invoice.total": "Total",
|
|
18813
19833
|
"financial.invoice.yesCancel": "Yes, Cancel",
|
|
18814
19834
|
"financial.invoiceForm.addFirstItem": "Add your first item",
|
|
19835
|
+
"financial.invoiceForm.tags": "Tags",
|
|
19836
|
+
"financial.invoiceForm.currency": "Currency",
|
|
18815
19837
|
"financial.invoiceForm.addItem": "Add Item",
|
|
18816
19838
|
"financial.invoiceForm.addItemsFirst": "Add items first",
|
|
18817
19839
|
"financial.invoiceForm.contactRequired": "Select who will pay or receive before adding items.",
|
|
@@ -18929,6 +19951,11 @@ var en = {
|
|
|
18929
19951
|
"financial.paymentReceipt.titleReversal": "Reversal details",
|
|
18930
19952
|
"financial.paymentReceipt.open": "See this payment's details",
|
|
18931
19953
|
"financial.paymentReceipt.count": "{{count}} payments",
|
|
19954
|
+
"financial.paymentReceipt.reversedCount": "{{count}} reversed payment(s)",
|
|
19955
|
+
"financial.paymentReceipt.show": "Show",
|
|
19956
|
+
"financial.paymentReceipt.hide": "Hide",
|
|
19957
|
+
"financial.paymentReceipt.reversedOn": "Reversed on {{date}}",
|
|
19958
|
+
"financial.paymentReceipt.reversed": "Reversed",
|
|
18932
19959
|
"financial.paymentReceipt.paymentNumber": "Payment {{number}}",
|
|
18933
19960
|
"financial.paymentReceipt.reversalNumber": "Reversal {{number}}",
|
|
18934
19961
|
"financial.paymentReceipt.empty": "No payment recorded on this installment.",
|
|
@@ -19209,6 +20236,9 @@ var en = {
|
|
|
19209
20236
|
"financial.settings.modulesDesc": "What this house shows inside Financial",
|
|
19210
20237
|
"financial.settings.commissions": "Commissions",
|
|
19211
20238
|
"financial.settings.commissionsDesc": "The tab that settles what the team earns per sale",
|
|
20239
|
+
"financial.settings.cashRegistersDesc": "Open, close and review the counter till",
|
|
20240
|
+
"financial.settings.closing": "Closing",
|
|
20241
|
+
"financial.settings.closingDesc": "Close the period and lock past dates",
|
|
19212
20242
|
"financial.settings.invoices": "Invoices",
|
|
19213
20243
|
"financial.settings.invoicesDesc": "How invoices and payments are handled",
|
|
19214
20244
|
"financial.settings.notifications": "Notifications",
|
|
@@ -19740,6 +20770,7 @@ var ptBR = {
|
|
|
19740
20770
|
"financial.invoice.reverseReason": "Motivo do estorno",
|
|
19741
20771
|
"financial.invoice.paymentSchedule": "Cronograma de Pagamento",
|
|
19742
20772
|
"financial.invoiceForm.scheduleLocked": "Parcelas j\xE1 pagas \u2014 o parcelamento n\xE3o pode mais ser refeito.",
|
|
20773
|
+
"financial.invoiceForm.scheduleHasCancelled": "Esta conta tem parcela cancelada \u2014 valor e parcelamento n\xE3o podem mais ser refeitos, s\xF3 o vencimento.",
|
|
19743
20774
|
"financial.invoiceForm.issuedLocked": "Conta emitida: valor, itens, fornecedor e data ficam travados. Para mudar, cancele e lance uma nova. Observa\xE7\xF5es, unidade, n\xBA do documento e vencimentos continuam edit\xE1veis.",
|
|
19744
20775
|
"financial.invoiceForm.issuedEditable": "Conta emitida: valor, itens e fornecedor ficam travados. Para mudar, cancele e lance uma nova. Enquanto nada for pago, a data e o parcelamento continuam edit\xE1veis.",
|
|
19745
20776
|
"financial.invoiceForm.edit": "Editar",
|
|
@@ -19754,6 +20785,8 @@ var ptBR = {
|
|
|
19754
20785
|
"financial.invoice.total": "Total",
|
|
19755
20786
|
"financial.invoice.yesCancel": "Sim, Cancelar",
|
|
19756
20787
|
"financial.invoiceForm.addFirstItem": "Adicione seu primeiro item",
|
|
20788
|
+
"financial.invoiceForm.tags": "Etiquetas",
|
|
20789
|
+
"financial.invoiceForm.currency": "Moeda",
|
|
19757
20790
|
"financial.invoiceForm.addItem": "Adicionar Item",
|
|
19758
20791
|
"financial.invoiceForm.addItemsFirst": "Adicione itens primeiro",
|
|
19759
20792
|
"financial.invoiceForm.contactRequired": "Selecione quem vai pagar ou receber antes de adicionar itens.",
|
|
@@ -19874,6 +20907,11 @@ var ptBR = {
|
|
|
19874
20907
|
"financial.paymentReceipt.titleReversal": "Detalhes do Estorno",
|
|
19875
20908
|
"financial.paymentReceipt.open": "Ver os detalhes deste pagamento",
|
|
19876
20909
|
"financial.paymentReceipt.count": "{{count}} pagamentos",
|
|
20910
|
+
"financial.paymentReceipt.reversedCount": "{{count}} pagamento(s) estornado(s)",
|
|
20911
|
+
"financial.paymentReceipt.show": "Ver",
|
|
20912
|
+
"financial.paymentReceipt.hide": "Ocultar",
|
|
20913
|
+
"financial.paymentReceipt.reversedOn": "Estornado em {{date}}",
|
|
20914
|
+
"financial.paymentReceipt.reversed": "Estornado",
|
|
19877
20915
|
"financial.paymentReceipt.paymentNumber": "Pagamento {{number}}",
|
|
19878
20916
|
"financial.paymentReceipt.reversalNumber": "Estorno {{number}}",
|
|
19879
20917
|
"financial.paymentReceipt.empty": "Nenhum pagamento registrado nesta parcela.",
|
|
@@ -20154,6 +21192,9 @@ var ptBR = {
|
|
|
20154
21192
|
"financial.settings.modulesDesc": "O que desta casa aparece no Financeiro",
|
|
20155
21193
|
"financial.settings.commissions": "Comiss\xF5es",
|
|
20156
21194
|
"financial.settings.commissionsDesc": "A aba de apura\xE7\xE3o do que a equipe recebe por venda",
|
|
21195
|
+
"financial.settings.cashRegistersDesc": "Abrir, fechar e conferir o caixa do balc\xE3o",
|
|
21196
|
+
"financial.settings.closing": "Fechamento",
|
|
21197
|
+
"financial.settings.closingDesc": "Fechar o per\xEDodo e travar datas passadas",
|
|
20157
21198
|
"financial.settings.invoices": "Faturas",
|
|
20158
21199
|
"financial.settings.invoicesDesc": "Como faturas e pagamentos s\xE3o processados",
|
|
20159
21200
|
"financial.settings.notifications": "Notifica\xE7\xF5es",
|
|
@@ -20657,14 +21698,14 @@ function CommissionRuleDialog({
|
|
|
20657
21698
|
onClose,
|
|
20658
21699
|
onSaved
|
|
20659
21700
|
}) {
|
|
20660
|
-
const [name, setName] =
|
|
20661
|
-
const [description, setDescription] =
|
|
20662
|
-
const [priority, setPriority] =
|
|
20663
|
-
const [isActive, setIsActive] =
|
|
20664
|
-
const [isDefault, setIsDefault] =
|
|
20665
|
-
const [saving, setSaving] =
|
|
20666
|
-
const [error, setError] =
|
|
20667
|
-
const initial =
|
|
21701
|
+
const [name, setName] = React29__default.useState(rule?.name ?? "");
|
|
21702
|
+
const [description, setDescription] = React29__default.useState(rule?.description ?? "");
|
|
21703
|
+
const [priority, setPriority] = React29__default.useState(String(rule?.priority ?? 1));
|
|
21704
|
+
const [isActive, setIsActive] = React29__default.useState(rule?.isActive ?? true);
|
|
21705
|
+
const [isDefault, setIsDefault] = React29__default.useState(rule?.isDefault ?? !hasDefault);
|
|
21706
|
+
const [saving, setSaving] = React29__default.useState(false);
|
|
21707
|
+
const [error, setError] = React29__default.useState(null);
|
|
21708
|
+
const initial = React29__default.useMemo(() => {
|
|
20668
21709
|
const get = (t2, key = "ids") => rule?.criteria.find((c) => c.criteriaType === t2)?.criteriaValue?.[key] ?? [];
|
|
20669
21710
|
return {
|
|
20670
21711
|
services: get("service"),
|
|
@@ -20675,12 +21716,12 @@ function CommissionRuleDialog({
|
|
|
20675
21716
|
ranges: get("time_range", "ranges")
|
|
20676
21717
|
};
|
|
20677
21718
|
}, [rule]);
|
|
20678
|
-
const [services, setServices] =
|
|
20679
|
-
const [categories, setCategories] =
|
|
20680
|
-
const [professionals, setProfessionals] =
|
|
20681
|
-
const [units, setUnits] =
|
|
20682
|
-
const [weekdays, setWeekdays] =
|
|
20683
|
-
const [ranges, setRanges] =
|
|
21719
|
+
const [services, setServices] = React29__default.useState(initial.services);
|
|
21720
|
+
const [categories, setCategories] = React29__default.useState(initial.categories);
|
|
21721
|
+
const [professionals, setProfessionals] = React29__default.useState(initial.professionals);
|
|
21722
|
+
const [units, setUnits] = React29__default.useState(initial.units);
|
|
21723
|
+
const [weekdays, setWeekdays] = React29__default.useState(initial.weekdays);
|
|
21724
|
+
const [ranges, setRanges] = React29__default.useState(initial.ranges);
|
|
20684
21725
|
const criteriaCount = [services, categories, professionals, units].filter((a) => a.length > 0).length + (weekdays.length ? 1 : 0) + (ranges.length ? 1 : 0);
|
|
20685
21726
|
async function handleSave() {
|
|
20686
21727
|
if (!name.trim()) {
|
|
@@ -20923,11 +21964,11 @@ function blankFunction(order) {
|
|
|
20923
21964
|
};
|
|
20924
21965
|
}
|
|
20925
21966
|
function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
|
|
20926
|
-
const [functions, setFunctions] =
|
|
21967
|
+
const [functions, setFunctions] = React29__default.useState(
|
|
20927
21968
|
rule.functions.length ? rule.functions : [blankFunction(0)]
|
|
20928
21969
|
);
|
|
20929
|
-
const [saving, setSaving] =
|
|
20930
|
-
const [error, setError] =
|
|
21970
|
+
const [saving, setSaving] = React29__default.useState(false);
|
|
21971
|
+
const [error, setError] = React29__default.useState(null);
|
|
20931
21972
|
function patch(i, changes) {
|
|
20932
21973
|
setFunctions((fs) => fs.map((f, j) => j === i ? { ...f, ...changes } : f));
|
|
20933
21974
|
}
|
|
@@ -21211,19 +22252,19 @@ function functionChip(fn) {
|
|
|
21211
22252
|
return fn.valueType === "percentage" ? `${who}: ${fn.valueAmount}%` : `${who}: R$ ${fn.valueAmount.toFixed(2)}`;
|
|
21212
22253
|
}
|
|
21213
22254
|
function CommissionRulesView({ provider }) {
|
|
21214
|
-
const [rules, setRules] =
|
|
21215
|
-
const [catalogs, setCatalogs] =
|
|
22255
|
+
const [rules, setRules] = React29__default.useState(null);
|
|
22256
|
+
const [catalogs, setCatalogs] = React29__default.useState({
|
|
21216
22257
|
services: [],
|
|
21217
22258
|
categories: [],
|
|
21218
22259
|
professionals: [],
|
|
21219
22260
|
units: []
|
|
21220
22261
|
});
|
|
21221
|
-
const [error, setError] =
|
|
21222
|
-
const [funnel, setFunnel] =
|
|
21223
|
-
const [editing, setEditing] =
|
|
21224
|
-
const [functionsFor, setFunctionsFor] =
|
|
21225
|
-
const [deleting, setDeleting] =
|
|
21226
|
-
const load =
|
|
22262
|
+
const [error, setError] = React29__default.useState(null);
|
|
22263
|
+
const [funnel, setFunnel] = React29__default.useState({});
|
|
22264
|
+
const [editing, setEditing] = React29__default.useState(null);
|
|
22265
|
+
const [functionsFor, setFunctionsFor] = React29__default.useState(null);
|
|
22266
|
+
const [deleting, setDeleting] = React29__default.useState(null);
|
|
22267
|
+
const load = React29__default.useCallback(async () => {
|
|
21227
22268
|
try {
|
|
21228
22269
|
setError(null);
|
|
21229
22270
|
const [r, c] = await Promise.all([
|
|
@@ -21237,17 +22278,17 @@ function CommissionRulesView({ provider }) {
|
|
|
21237
22278
|
setRules([]);
|
|
21238
22279
|
}
|
|
21239
22280
|
}, [provider]);
|
|
21240
|
-
|
|
22281
|
+
React29__default.useEffect(() => {
|
|
21241
22282
|
void load();
|
|
21242
22283
|
}, [load]);
|
|
21243
22284
|
const funnelActive = Object.values(funnel).some(Boolean);
|
|
21244
|
-
const visible =
|
|
22285
|
+
const visible = React29__default.useMemo(() => {
|
|
21245
22286
|
if (!rules) return [];
|
|
21246
22287
|
const ranked = rankRules(rules);
|
|
21247
22288
|
if (!funnelActive) return ranked;
|
|
21248
22289
|
return ranked.filter((r) => r.isDefault || ruleMatches(r, funnel, catalogs));
|
|
21249
22290
|
}, [rules, funnel, funnelActive, catalogs]);
|
|
21250
|
-
const winner =
|
|
22291
|
+
const winner = React29__default.useMemo(() => {
|
|
21251
22292
|
if (!funnelActive) return null;
|
|
21252
22293
|
return visible.find((r) => r.isActive && !r.isDefault) ?? visible.find((r) => r.isActive && r.isDefault) ?? null;
|
|
21253
22294
|
}, [visible, funnelActive]);
|
|
@@ -21262,7 +22303,7 @@ function CommissionRulesView({ provider }) {
|
|
|
21262
22303
|
await provider.duplicateCommissionRule?.(rule.id);
|
|
21263
22304
|
await load();
|
|
21264
22305
|
}
|
|
21265
|
-
const headerActions =
|
|
22306
|
+
const headerActions = React29__default.useMemo(() => [{
|
|
21266
22307
|
id: "new-rule",
|
|
21267
22308
|
label: "Adicionar regra",
|
|
21268
22309
|
icon: Plus,
|
|
@@ -21548,7 +22589,7 @@ function methodsDetail(value) {
|
|
|
21548
22589
|
var stripeConnectorDef = {
|
|
21549
22590
|
id: STRIPE_CONNECTOR_ID,
|
|
21550
22591
|
hostPluginId: "financial",
|
|
21551
|
-
name: "Stripe",
|
|
22592
|
+
name: "Stripe \xB7 Faturas",
|
|
21552
22593
|
description: "Emita as faturas do Financeiro na Stripe e receba a baixa quando o cliente pagar.",
|
|
21553
22594
|
icon: "CreditCard",
|
|
21554
22595
|
category: "payments",
|
|
@@ -21726,7 +22767,7 @@ var store2 = createConnectionStore(MERCADOPAGO_CONNECTOR_ID, {
|
|
|
21726
22767
|
var mercadoPagoConnectorDef = {
|
|
21727
22768
|
id: MERCADOPAGO_CONNECTOR_ID,
|
|
21728
22769
|
hostPluginId: "financial",
|
|
21729
|
-
name: "Mercado Pago",
|
|
22770
|
+
name: "Mercado Pago \xB7 Recebimentos",
|
|
21730
22771
|
description: "Traga para o Financeiro tudo que entrou no Mercado Pago \u2014 Pix, cart\xE3o e maquininha \u2014 com a taxa e a data do repasse.",
|
|
21731
22772
|
icon: "CreditCard",
|
|
21732
22773
|
category: "payments",
|
|
@@ -21960,7 +23001,7 @@ var STATUS_STYLE = {
|
|
|
21960
23001
|
};
|
|
21961
23002
|
function PersonStatementWidget({ person, provider, config }) {
|
|
21962
23003
|
const t2 = useTranslation();
|
|
21963
|
-
const currency = config.currency;
|
|
23004
|
+
const currency = useCurrency(config.currency);
|
|
21964
23005
|
const [invoices, setInvoices] = useState([]);
|
|
21965
23006
|
const [loading, setLoading] = useState(true);
|
|
21966
23007
|
useEffect(() => {
|
|
@@ -21982,13 +23023,21 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
21982
23023
|
alive = false;
|
|
21983
23024
|
};
|
|
21984
23025
|
}, [person?.id, provider]);
|
|
21985
|
-
const fmt = (n) => formatCurrency(n, currency);
|
|
23026
|
+
const fmt = (n, code) => formatCurrency(n, code && code !== currency.code ? resolveCurrency(config.currency, code) : currency);
|
|
21986
23027
|
const totals = useMemo(() => {
|
|
21987
|
-
const
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
23028
|
+
const by = /* @__PURE__ */ new Map();
|
|
23029
|
+
for (const i of invoices) {
|
|
23030
|
+
if (i.status === "cancelled") continue;
|
|
23031
|
+
const code = i.currency || currency.code;
|
|
23032
|
+
const sum = by.get(code) ?? { billed: 0, paid: 0 };
|
|
23033
|
+
sum.billed += i.totalAmount ?? 0;
|
|
23034
|
+
sum.paid += i.paidAmount ?? 0;
|
|
23035
|
+
by.set(code, sum);
|
|
23036
|
+
}
|
|
23037
|
+
if (by.size === 0) by.set(currency.code, { billed: 0, paid: 0 });
|
|
23038
|
+
const line = (pick) => [...by].map(([code, sum]) => fmt(pick(sum), code)).join(" + ");
|
|
23039
|
+
return { billed: line((s) => s.billed), paid: line((s) => s.paid), balance: line((s) => s.billed - s.paid) };
|
|
23040
|
+
}, [invoices, currency]);
|
|
21992
23041
|
const openInvoice = (inv) => {
|
|
21993
23042
|
const base = inv.direction === "credit" ? "receivables" : "payables";
|
|
21994
23043
|
window.location.hash = `/financial/${base}/detail/${inv.id}`;
|
|
@@ -22013,17 +23062,17 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
22013
23062
|
{
|
|
22014
23063
|
id: "amount",
|
|
22015
23064
|
header: t2("financial.invoice.columnAmount"),
|
|
22016
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `text-right block text-xs ${row.original.direction === "credit" ? "text-success" : "text-destructive"}`, children: fmt(row.original.totalAmount) })
|
|
23065
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `text-right block text-xs ${row.original.direction === "credit" ? "text-success" : "text-destructive"}`, children: fmt(row.original.totalAmount, row.original.currency) })
|
|
22017
23066
|
},
|
|
22018
23067
|
{
|
|
22019
23068
|
id: "paid",
|
|
22020
23069
|
header: t2("financial.invoice.columnPaid"),
|
|
22021
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs text-muted-foreground", children: fmt(row.original.paidAmount) })
|
|
23070
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs text-muted-foreground", children: fmt(row.original.paidAmount, row.original.currency) })
|
|
22022
23071
|
},
|
|
22023
23072
|
{
|
|
22024
23073
|
id: "balance",
|
|
22025
23074
|
header: t2("financial.statements.columnBalance"),
|
|
22026
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs font-medium", children: fmt((row.original.totalAmount ?? 0) - (row.original.paidAmount ?? 0)) })
|
|
23075
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs font-medium", children: fmt((row.original.totalAmount ?? 0) - (row.original.paidAmount ?? 0), row.original.currency) })
|
|
22027
23076
|
},
|
|
22028
23077
|
{
|
|
22029
23078
|
id: "status",
|
|
@@ -22033,9 +23082,9 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
22033
23082
|
], [currency, t2]);
|
|
22034
23083
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
22035
23084
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
|
|
22036
|
-
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryTotal"), value:
|
|
22037
|
-
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryPaid"), value:
|
|
22038
|
-
/* @__PURE__ */ jsx(Stat, { label: t2("financial.statements.columnBalance"), value:
|
|
23085
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryTotal"), value: totals.billed }),
|
|
23086
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryPaid"), value: totals.paid, positive: true }),
|
|
23087
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.statements.columnBalance"), value: totals.balance, accent: true })
|
|
22039
23088
|
] }),
|
|
22040
23089
|
loading ? /* @__PURE__ */ jsx(DataTable, { columns, data: [], loading: true, skeletonRows: 5, compact: true }) : invoices.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-10 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
22041
23090
|
/* @__PURE__ */ jsx(Wallet, { className: "h-7 w-7 text-muted-foreground mb-2" }),
|
|
@@ -22044,7 +23093,7 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
22044
23093
|
] });
|
|
22045
23094
|
}
|
|
22046
23095
|
function SupplierFinancialSettings({ person, provider }) {
|
|
22047
|
-
const initial =
|
|
23096
|
+
const initial = React29__default.useMemo(() => counterpartyAccountingPolicy(person), [person]);
|
|
22048
23097
|
const [accounts, setAccounts] = useState([]);
|
|
22049
23098
|
const [costCenters, setCostCenters] = useState([]);
|
|
22050
23099
|
const [defaultAccountId, setDefaultAccountId] = useState(initial.defaultAccountId ?? "");
|
|
@@ -45020,7 +46069,11 @@ SELECT app.scaffold_scope('public.plg_financial_account_statement_sources'::regc
|
|
|
45020
46069
|
-- fazia a lista nascer vazia: \`getBankAccounts\` varria o plano INTEIRO ordenado
|
|
45021
46070
|
-- por nome, e num inquilino com milhares de contas de pessoa o teto da resposta
|
|
45022
46071
|
-- se enchia antes de chegar em \xABIta\xFA\xBB.
|
|
45023
|
-
CREATE OR REPLACE
|
|
46072
|
+
-- DROP, then CREATE: the view selects \`account.*\`, and OR REPLACE refuses to
|
|
46073
|
+
-- shift columns when the table gains one (124 added \`currency\`), so a replay
|
|
46074
|
+
-- of this file failed on any pool that had moved on.
|
|
46075
|
+
DROP VIEW IF EXISTS public.v_financial_statement_accounts;
|
|
46076
|
+
CREATE VIEW public.v_financial_statement_accounts
|
|
45024
46077
|
WITH (security_invoker = true) AS
|
|
45025
46078
|
SELECT account.*,
|
|
45026
46079
|
source.provider,
|
|
@@ -46495,6 +47548,938 @@ BEGIN
|
|
|
46495
47548
|
END LOOP;
|
|
46496
47549
|
END $patch$;
|
|
46497
47550
|
`;
|
|
47551
|
+
var MIGRATION_123_THE_MIRROR_DOES_NOT_BILL_A_RECURRENCE_TWICE = `-- ============================================================================
|
|
47552
|
+
-- 123 \u2014 the mirror does not bill a recurrence twice
|
|
47553
|
+
--
|
|
47554
|
+
-- A tenant in coexistence has every recurring bill on both sides. When someone
|
|
47555
|
+
-- issues October in V2 and V1 then generates its own October bill, the mirror
|
|
47556
|
+
-- brought the V1 bill in as a second payable: same supplier, same amount, same
|
|
47557
|
+
-- due date, nothing tying the two together (Artorius, 2026-10-11: four pairs,
|
|
47558
|
+
-- R$ 16.599,50 owed twice on screen).
|
|
47559
|
+
--
|
|
47560
|
+
-- The tie exists, it was just never read: the occurrence is the SAME row on
|
|
47561
|
+
-- both sides, V2 stamps \`invoice_id\` on it when it issues, and V1 reports the
|
|
47562
|
+
-- bill it generated as \`metadata.v1.invoice_id\`. An occurrence carrying both
|
|
47563
|
+
-- names two bills for one month. The V2 one stays \u2014 it is the one somebody
|
|
47564
|
+
-- chose to issue \u2014 and the unpaid mirrored copy is cancelled as a V2 decision,
|
|
47565
|
+
-- so later passes of the mirror do not bring it back. A copy that already has
|
|
47566
|
+
-- a payment is left alone: which of two paid bills is wrong is not a sync's
|
|
47567
|
+
-- call.
|
|
47568
|
+
--
|
|
47569
|
+
-- Both writers ask, because either row can arrive last: the bill usually lands
|
|
47570
|
+
-- seconds before its occurrence is updated, but a bill held in quarantine
|
|
47571
|
+
-- lands batches later, when the occurrence no longer changes.
|
|
47572
|
+
-- ============================================================================
|
|
47573
|
+
|
|
47574
|
+
CREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_v1_invoice_idx
|
|
47575
|
+
ON public.plg_financial_recurring_bill_occurrences (tenant_id, ((metadata -> 'v1' ->> 'invoice_id')))
|
|
47576
|
+
WHERE invoice_id IS NOT NULL;
|
|
47577
|
+
|
|
47578
|
+
CREATE OR REPLACE FUNCTION migration._fin_cancel_recurring_duplicate(p_tenant uuid, p_v1_invoice text)
|
|
47579
|
+
RETURNS uuid
|
|
47580
|
+
LANGUAGE plpgsql
|
|
47581
|
+
SECURITY DEFINER
|
|
47582
|
+
SET search_path TO ''
|
|
47583
|
+
AS $function$
|
|
47584
|
+
DECLARE
|
|
47585
|
+
v_kept uuid;
|
|
47586
|
+
v_number text;
|
|
47587
|
+
v_dup uuid;
|
|
47588
|
+
BEGIN
|
|
47589
|
+
IF nullif(btrim(coalesce(p_v1_invoice, '')), '') IS NULL THEN
|
|
47590
|
+
RETURN NULL;
|
|
47591
|
+
END IF;
|
|
47592
|
+
|
|
47593
|
+
SELECT f.id, f.number INTO v_kept, v_number
|
|
47594
|
+
FROM public.plg_financial_recurring_bill_occurrences o
|
|
47595
|
+
JOIN public.plg_financial_invoices f ON f.id = o.invoice_id AND f.cancelled_at IS NULL
|
|
47596
|
+
WHERE o.tenant_id = p_tenant
|
|
47597
|
+
AND o.invoice_id IS NOT NULL
|
|
47598
|
+
AND o.metadata -> 'v1' ->> 'invoice_id' = p_v1_invoice
|
|
47599
|
+
LIMIT 1;
|
|
47600
|
+
IF v_kept IS NULL THEN
|
|
47601
|
+
RETURN NULL;
|
|
47602
|
+
END IF;
|
|
47603
|
+
|
|
47604
|
+
SELECT f.id INTO v_dup
|
|
47605
|
+
FROM public.plg_financial_invoices f
|
|
47606
|
+
WHERE f.tenant_id = p_tenant
|
|
47607
|
+
AND f.id <> v_kept
|
|
47608
|
+
AND f.cancelled_at IS NULL
|
|
47609
|
+
AND f.metadata -> 'migration' ->> 'source_table' = 'invoices'
|
|
47610
|
+
AND f.metadata -> 'migration' ->> 'source_numeric_id' = p_v1_invoice
|
|
47611
|
+
AND NOT EXISTS (SELECT 1
|
|
47612
|
+
FROM public.plg_financial_invoice_installments i
|
|
47613
|
+
JOIN public.plg_financial_allocations a ON a.installment_id = i.id
|
|
47614
|
+
WHERE i.invoice_id = f.id)
|
|
47615
|
+
LIMIT 1;
|
|
47616
|
+
IF v_dup IS NULL THEN
|
|
47617
|
+
RETURN NULL;
|
|
47618
|
+
END IF;
|
|
47619
|
+
|
|
47620
|
+
-- Same three writes as the unpaid branch of financial_cancel_invoice_items,
|
|
47621
|
+
-- which cannot be called here: it reads the tenant from a session.
|
|
47622
|
+
UPDATE public.plg_financial_invoices
|
|
47623
|
+
SET cancelled_at = now(),
|
|
47624
|
+
cancel_reason = 'Duplicata: a mesma ocorr\xEAncia da recorr\xEAncia j\xE1 foi lan\xE7ada como ' || v_number,
|
|
47625
|
+
metadata = metadata || jsonb_build_object('v2_decision',
|
|
47626
|
+
jsonb_build_object('cancelled', true, 'at', now(), 'duplicate_of', v_kept))
|
|
47627
|
+
WHERE id = v_dup;
|
|
47628
|
+
UPDATE public.plg_financial_invoice_installments
|
|
47629
|
+
SET cancelled_at = now(),
|
|
47630
|
+
metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true))
|
|
47631
|
+
WHERE invoice_id = v_dup AND cancelled_at IS NULL;
|
|
47632
|
+
UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()
|
|
47633
|
+
WHERE financial_invoice_id = v_dup AND movement_kind = 'bill' AND status <> 'cancelled';
|
|
47634
|
+
|
|
47635
|
+
RETURN v_dup;
|
|
47636
|
+
END $function$;
|
|
47637
|
+
REVOKE ALL ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) FROM PUBLIC, anon, authenticated;
|
|
47638
|
+
GRANT EXECUTE ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) TO service_role;
|
|
47639
|
+
|
|
47640
|
+
-- The two writers are patched in place, as 122 patched the money RPCs: their
|
|
47641
|
+
-- live bodies differ from the files that created them.
|
|
47642
|
+
DO $patch$
|
|
47643
|
+
DECLARE
|
|
47644
|
+
v_fn regprocedure;
|
|
47645
|
+
v_def text;
|
|
47646
|
+
v_anchor text;
|
|
47647
|
+
v_call text;
|
|
47648
|
+
BEGIN
|
|
47649
|
+
FOR v_fn, v_anchor, v_call IN
|
|
47650
|
+
SELECT f::regprocedure, a, c FROM (VALUES
|
|
47651
|
+
('migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb)',
|
|
47652
|
+
' IF v_standalone THEN PERFORM migration._audit(p_tenant, ''public.plg_financial_recurring_bill_occurrences'', p_id, ''{}''::jsonb); END IF;',
|
|
47653
|
+
' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, v_meta -> ''v1'' ->> ''invoice_id'');'),
|
|
47654
|
+
('migration.upsert_invoice(uuid, uuid, jsonb, jsonb)',
|
|
47655
|
+
' IF v_standalone THEN PERFORM migration._audit(p_tenant, v_table, p_id, jsonb_build_object(''reason'', ''created'', ''installments'', v_n)); END IF;',
|
|
47656
|
+
' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, p_row -> ''metadata'' -> ''migration'' ->> ''source_numeric_id'');')
|
|
47657
|
+
) AS t(f, a, c)
|
|
47658
|
+
LOOP
|
|
47659
|
+
v_def := pg_get_functiondef(v_fn);
|
|
47660
|
+
IF position('_fin_cancel_recurring_duplicate' in v_def) > 0 THEN
|
|
47661
|
+
CONTINUE; -- already asks (a replay, or a later rewrite)
|
|
47662
|
+
END IF;
|
|
47663
|
+
IF position(v_anchor in v_def) = 0 THEN
|
|
47664
|
+
RAISE EXCEPTION '123: % no longer has the line the duplicate check goes before (%); patch it by hand', v_fn, v_anchor;
|
|
47665
|
+
END IF;
|
|
47666
|
+
EXECUTE overlay(v_def PLACING v_call || E'\\n' || v_anchor
|
|
47667
|
+
FROM position(v_anchor in v_def) FOR length(v_anchor));
|
|
47668
|
+
END LOOP;
|
|
47669
|
+
END $patch$;
|
|
47670
|
+
`;
|
|
47671
|
+
var MIGRATION_124_MONEY_KNOWS_ITS_CURRENCY = `-- ============================================================================
|
|
47672
|
+
-- 124 \u2014 money knows its currency
|
|
47673
|
+
--
|
|
47674
|
+
-- Invoices carried a \`currency\` nobody set (always the 'BRL' default) and the
|
|
47675
|
+
-- ledger had none at all: an account, a payment and a balance were bare
|
|
47676
|
+
-- numbers. One tenant with a shop in Rio and one in Barcelona would have had
|
|
47677
|
+
-- 30 EUR and 30 BRL add up to 60.
|
|
47678
|
+
--
|
|
47679
|
+
-- \xB7 an invoice takes the currency of the order it bills, else its unit's;
|
|
47680
|
+
-- \xB7 a bank or cash account has ONE currency (its unit's, unless said);
|
|
47681
|
+
-- \xB7 a money movement is in the currency of the account the money went
|
|
47682
|
+
-- through, and may not settle an invoice written in another one;
|
|
47683
|
+
-- \xB7 every movement and invoice is stamped with the day's rate to the
|
|
47684
|
+
-- tenant's own currency, when a rate is on file, so a report converted
|
|
47685
|
+
-- today still says tomorrow what it said today.
|
|
47686
|
+
--
|
|
47687
|
+
-- Rates are data the tenant keeps (\`plg_financial_fx_rates\`). Nothing here
|
|
47688
|
+
-- fetches one, and nothing here converts a report: the reading side comes
|
|
47689
|
+
-- after the data is right.
|
|
47690
|
+
--
|
|
47691
|
+
-- Additive. Every existing row reads as BRL, which is what it was.
|
|
47692
|
+
-- ============================================================================
|
|
47693
|
+
|
|
47694
|
+
-- \u2500\u2500 1. rates \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
47695
|
+
CREATE TABLE IF NOT EXISTS public.plg_financial_fx_rates (
|
|
47696
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
47697
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
47698
|
+
rate_date date NOT NULL,
|
|
47699
|
+
base_currency text NOT NULL CHECK (base_currency ~ '^[A-Z]{3}$'),
|
|
47700
|
+
quote_currency text NOT NULL CHECK (quote_currency ~ '^[A-Z]{3}$'),
|
|
47701
|
+
rate numeric NOT NULL CHECK (rate > 0),
|
|
47702
|
+
source text,
|
|
47703
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
47704
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
47705
|
+
CHECK (base_currency <> quote_currency)
|
|
47706
|
+
);
|
|
47707
|
+
COMMENT ON TABLE public.plg_financial_fx_rates IS '1 base_currency = rate quote_currency, on rate_date.';
|
|
47708
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_fx_rates_day_uq
|
|
47709
|
+
ON public.plg_financial_fx_rates (tenant_id, base_currency, quote_currency, rate_date);
|
|
47710
|
+
|
|
47711
|
+
DO $$
|
|
47712
|
+
BEGIN
|
|
47713
|
+
CREATE TRIGGER plg_financial_fx_rates_updated_at
|
|
47714
|
+
BEFORE UPDATE ON public.plg_financial_fx_rates
|
|
47715
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
47716
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
47717
|
+
END $$;
|
|
47718
|
+
|
|
47719
|
+
SELECT app.scaffold_table('public.plg_financial_fx_rates'::regclass,
|
|
47720
|
+
'financial.invoice', 'financial', false, 'enforce');
|
|
47721
|
+
SELECT app.scaffold_scope('public.plg_financial_fx_rates'::regclass, 'tenant', true);
|
|
47722
|
+
|
|
47723
|
+
-- The latest rate on or before the day, in either direction. NULL when the
|
|
47724
|
+
-- tenant has none on file: a missing rate is not 1.
|
|
47725
|
+
CREATE OR REPLACE FUNCTION public.plg_financial_fx_rate(p_tenant uuid, p_from text, p_to text, p_on date DEFAULT NULL)
|
|
47726
|
+
RETURNS numeric
|
|
47727
|
+
LANGUAGE sql
|
|
47728
|
+
STABLE SECURITY DEFINER
|
|
47729
|
+
SET search_path TO ''
|
|
47730
|
+
AS $function$
|
|
47731
|
+
SELECT CASE
|
|
47732
|
+
WHEN p_from IS NULL OR p_to IS NULL THEN NULL
|
|
47733
|
+
WHEN upper(p_from) = upper(p_to) THEN 1::numeric
|
|
47734
|
+
ELSE (
|
|
47735
|
+
SELECT CASE WHEN r.base_currency = upper(p_from) THEN r.rate ELSE round(1 / r.rate, 8) END
|
|
47736
|
+
FROM public.plg_financial_fx_rates r
|
|
47737
|
+
WHERE r.tenant_id = p_tenant
|
|
47738
|
+
AND r.rate_date <= coalesce(p_on, current_date)
|
|
47739
|
+
AND ((r.base_currency = upper(p_from) AND r.quote_currency = upper(p_to))
|
|
47740
|
+
OR (r.base_currency = upper(p_to) AND r.quote_currency = upper(p_from)))
|
|
47741
|
+
ORDER BY r.rate_date DESC, (r.base_currency = upper(p_from)) DESC
|
|
47742
|
+
LIMIT 1)
|
|
47743
|
+
END;
|
|
47744
|
+
$function$;
|
|
47745
|
+
REVOKE ALL ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) FROM PUBLIC, anon, authenticated;
|
|
47746
|
+
GRANT EXECUTE ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) TO service_role;
|
|
47747
|
+
|
|
47748
|
+
-- \u2500\u2500 2. columns \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
47749
|
+
-- Added with the default so every existing row becomes BRL without a rewrite,
|
|
47750
|
+
-- then the default goes: a new row is filled by its trigger, from its unit.
|
|
47751
|
+
DO $$
|
|
47752
|
+
BEGIN
|
|
47753
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
47754
|
+
WHERE table_schema = 'public' AND table_name = 'plg_financial_accounts' AND column_name = 'currency') THEN
|
|
47755
|
+
ALTER TABLE public.plg_financial_accounts ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
47756
|
+
ALTER TABLE public.plg_financial_accounts ALTER COLUMN currency DROP DEFAULT;
|
|
47757
|
+
END IF;
|
|
47758
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
47759
|
+
WHERE table_schema = 'public' AND table_name = 'plg_financial_movements' AND column_name = 'currency') THEN
|
|
47760
|
+
ALTER TABLE public.plg_financial_movements ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
47761
|
+
ALTER TABLE public.plg_financial_movements ALTER COLUMN currency DROP DEFAULT;
|
|
47762
|
+
END IF;
|
|
47763
|
+
END $$;
|
|
47764
|
+
ALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS fx_rate numeric;
|
|
47765
|
+
ALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS base_amount numeric(14,2);
|
|
47766
|
+
-- A pool that took the first cut of this file has it bare; money declares its scale.
|
|
47767
|
+
DO $scale$
|
|
47768
|
+
BEGIN
|
|
47769
|
+
IF (SELECT format_type(a.atttypid, a.atttypmod) FROM pg_attribute a
|
|
47770
|
+
WHERE a.attrelid = 'public.plg_financial_movements'::regclass AND a.attname = 'base_amount') = 'numeric' THEN
|
|
47771
|
+
ALTER TABLE public.plg_financial_movements ALTER COLUMN base_amount TYPE numeric(14,2);
|
|
47772
|
+
END IF;
|
|
47773
|
+
END $scale$;
|
|
47774
|
+
ALTER TABLE public.plg_financial_invoices ADD COLUMN IF NOT EXISTS fx_rate numeric;
|
|
47775
|
+
ALTER TABLE public.plg_financial_invoices ALTER COLUMN currency DROP DEFAULT;
|
|
47776
|
+
|
|
47777
|
+
COMMENT ON COLUMN public.plg_financial_accounts.currency IS 'The one currency this account holds. Money in another currency arrives by a transfer with a rate.';
|
|
47778
|
+
COMMENT ON COLUMN public.plg_financial_movements.currency IS 'Currency of \`amount\`: the account the money went through.';
|
|
47779
|
+
COMMENT ON COLUMN public.plg_financial_movements.fx_rate IS 'Rate to the tenant''s own currency on the day, when one was on file.';
|
|
47780
|
+
COMMENT ON COLUMN public.plg_financial_movements.base_amount IS 'amount \xD7 fx_rate, stored: what a consolidated report sums.';
|
|
47781
|
+
COMMENT ON COLUMN public.plg_financial_invoices.fx_rate IS 'Rate to the tenant''s own currency on the issue date, when one was on file.';
|
|
47782
|
+
|
|
47783
|
+
-- \u2500\u2500 3. an account is born in its unit's currency \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
|
|
47784
|
+
CREATE OR REPLACE FUNCTION public.trg_plg_financial_accounts_currency()
|
|
47785
|
+
RETURNS trigger
|
|
47786
|
+
LANGUAGE plpgsql
|
|
47787
|
+
SECURITY DEFINER
|
|
47788
|
+
SET search_path TO ''
|
|
47789
|
+
AS $function$
|
|
47790
|
+
BEGIN
|
|
47791
|
+
IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
|
|
47792
|
+
NEW.currency := app.unit_currency(NEW.tenant_id, NEW.unit_id);
|
|
47793
|
+
ELSE
|
|
47794
|
+
NEW.currency := upper(btrim(NEW.currency));
|
|
47795
|
+
END IF;
|
|
47796
|
+
RETURN NEW;
|
|
47797
|
+
END $function$;
|
|
47798
|
+
DROP TRIGGER IF EXISTS plg_financial_accounts_currency ON public.plg_financial_accounts;
|
|
47799
|
+
CREATE TRIGGER plg_financial_accounts_currency
|
|
47800
|
+
BEFORE INSERT ON public.plg_financial_accounts
|
|
47801
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_accounts_currency();
|
|
47802
|
+
|
|
47803
|
+
-- \u2500\u2500 4. an invoice is in the currency of what it bills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
47804
|
+
CREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()
|
|
47805
|
+
RETURNS trigger
|
|
47806
|
+
LANGUAGE plpgsql
|
|
47807
|
+
SECURITY DEFINER
|
|
47808
|
+
SET search_path TO ''
|
|
47809
|
+
AS $function$
|
|
47810
|
+
BEGIN
|
|
47811
|
+
IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
|
|
47812
|
+
IF NEW.order_id IS NOT NULL THEN
|
|
47813
|
+
SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;
|
|
47814
|
+
END IF;
|
|
47815
|
+
NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));
|
|
47816
|
+
END IF;
|
|
47817
|
+
NEW.currency := upper(btrim(NEW.currency));
|
|
47818
|
+
IF NEW.fx_rate IS NULL THEN
|
|
47819
|
+
NEW.fx_rate := public.plg_financial_fx_rate(
|
|
47820
|
+
NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));
|
|
47821
|
+
END IF;
|
|
47822
|
+
RETURN NEW;
|
|
47823
|
+
END $function$;
|
|
47824
|
+
-- "zz": after \`plg_financial_invoices_default_issuer\`, which may set the unit.
|
|
47825
|
+
DROP TRIGGER IF EXISTS plg_financial_invoices_zz_currency ON public.plg_financial_invoices;
|
|
47826
|
+
CREATE TRIGGER plg_financial_invoices_zz_currency
|
|
47827
|
+
BEFORE INSERT ON public.plg_financial_invoices
|
|
47828
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoices_currency();
|
|
47829
|
+
|
|
47830
|
+
-- \u2500\u2500 5. a movement is in the currency of the account it went through \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
47831
|
+
CREATE OR REPLACE FUNCTION public.trg_plg_financial_movements_currency()
|
|
47832
|
+
RETURNS trigger
|
|
47833
|
+
LANGUAGE plpgsql
|
|
47834
|
+
SECURITY DEFINER
|
|
47835
|
+
SET search_path TO ''
|
|
47836
|
+
AS $function$
|
|
47837
|
+
DECLARE
|
|
47838
|
+
v_holder text;
|
|
47839
|
+
v_invoice text;
|
|
47840
|
+
BEGIN
|
|
47841
|
+
IF NEW.financial_invoice_id IS NOT NULL THEN
|
|
47842
|
+
SELECT i.currency INTO v_invoice FROM public.plg_financial_invoices i WHERE i.id = NEW.financial_invoice_id;
|
|
47843
|
+
END IF;
|
|
47844
|
+
|
|
47845
|
+
-- The holder is the leg that actually keeps money. System and person
|
|
47846
|
+
-- accounts are bookkeeping counterparts and have no currency of their own.
|
|
47847
|
+
SELECT a.currency INTO v_holder
|
|
47848
|
+
FROM public.plg_financial_accounts a
|
|
47849
|
+
WHERE a.id IN (NEW.debit_account_id, NEW.credit_account_id)
|
|
47850
|
+
AND a.kind NOT IN ('system', 'person')
|
|
47851
|
+
ORDER BY (a.id = NEW.bank_account_id) DESC
|
|
47852
|
+
LIMIT 1;
|
|
47853
|
+
|
|
47854
|
+
IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
|
|
47855
|
+
NEW.currency := coalesce(v_holder, v_invoice, app.unit_currency(NEW.tenant_id, NEW.unit_id));
|
|
47856
|
+
END IF;
|
|
47857
|
+
NEW.currency := upper(btrim(NEW.currency));
|
|
47858
|
+
|
|
47859
|
+
-- A reversal mirrors the movement it undoes and is never the one refused.
|
|
47860
|
+
IF NEW.movement_kind = 'payment' AND NEW.reverses_id IS NULL
|
|
47861
|
+
AND v_holder IS NOT NULL AND v_invoice IS NOT NULL AND v_holder <> v_invoice THEN
|
|
47862
|
+
RAISE EXCEPTION 'Esta conta \xE9 em % e a conta banc\xE1ria escolhida \xE9 em %. Pague por uma conta em % ou transfira com c\xE2mbio antes.',
|
|
47863
|
+
v_invoice, v_holder, v_invoice
|
|
47864
|
+
USING ERRCODE = '23514';
|
|
47865
|
+
END IF;
|
|
47866
|
+
|
|
47867
|
+
IF NEW.fx_rate IS NULL THEN
|
|
47868
|
+
NEW.fx_rate := public.plg_financial_fx_rate(
|
|
47869
|
+
NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.payment_date, NEW.due_date, current_date));
|
|
47870
|
+
END IF;
|
|
47871
|
+
IF NEW.base_amount IS NULL AND NEW.fx_rate IS NOT NULL THEN
|
|
47872
|
+
NEW.base_amount := round(NEW.amount * NEW.fx_rate, 2);
|
|
47873
|
+
END IF;
|
|
47874
|
+
RETURN NEW;
|
|
47875
|
+
END $function$;
|
|
47876
|
+
-- Named to fire after \`plg_financial_movements_before_insert\`, which is the
|
|
47877
|
+
-- one that resolves the two legs this reads.
|
|
47878
|
+
DROP TRIGGER IF EXISTS plg_financial_movements_currency ON public.plg_financial_movements;
|
|
47879
|
+
CREATE TRIGGER plg_financial_movements_currency
|
|
47880
|
+
BEFORE INSERT ON public.plg_financial_movements
|
|
47881
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_currency();
|
|
47882
|
+
`;
|
|
47883
|
+
var MIGRATION_125_THE_SUMMARY_ADDS_ONE_CURRENCY_AT_A_TIME = `-- ============================================================================
|
|
47884
|
+
-- 125 \u2014 the summary adds one currency at a time
|
|
47885
|
+
--
|
|
47886
|
+
-- \`financial_summary\` summed every amount it could see. With a purchase in
|
|
47887
|
+
-- reais and a sale in euros in the same tenant, the dashboard showed their sum
|
|
47888
|
+
-- as one number in whichever currency the screen printed.
|
|
47889
|
+
--
|
|
47890
|
+
-- The arithmetic now runs per currency (\`financial_summary_by_currency\`: one
|
|
47891
|
+
-- row each, with today's rate to the tenant's own currency), and
|
|
47892
|
+
-- \`financial_summary\` \u2014 same name, same nine columns, so every screen that
|
|
47893
|
+
-- reads it keeps working \u2014 returns the converted total.
|
|
47894
|
+
--
|
|
47895
|
+
-- An amount in a currency with no rate on file is LEFT OUT of that total, not
|
|
47896
|
+
-- counted at 1:1: the per-currency rows still carry it, and the screen says so.
|
|
47897
|
+
-- A tenant with one currency gets the row it always got, at rate 1.
|
|
47898
|
+
--
|
|
47899
|
+
-- The fence (who sees which movement and account) is the body of 077/085/086,
|
|
47900
|
+
-- unchanged.
|
|
47901
|
+
-- ============================================================================
|
|
47902
|
+
|
|
47903
|
+
CREATE OR REPLACE FUNCTION public.financial_summary_by_currency(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)
|
|
47904
|
+
RETURNS TABLE(currency text, house_currency text, fx_rate numeric,
|
|
47905
|
+
total_balance numeric, total_receivable numeric, total_payable numeric,
|
|
47906
|
+
monthly_inflow numeric, monthly_outflow numeric,
|
|
47907
|
+
overdue_receivable_count integer, overdue_receivable_amount numeric,
|
|
47908
|
+
overdue_payable_count integer, overdue_payable_amount numeric)
|
|
47909
|
+
LANGUAGE sql
|
|
47910
|
+
STABLE SECURITY DEFINER
|
|
47911
|
+
SET search_path TO ''
|
|
47912
|
+
AS $function$
|
|
47913
|
+
WITH ctx AS (
|
|
47914
|
+
SELECT app.current_tenant_id() AS tenant,
|
|
47915
|
+
auth.uid() AS me,
|
|
47916
|
+
app.visible_units() AS units,
|
|
47917
|
+
app.has_permission('financial.read', NULL::uuid) AS read_null,
|
|
47918
|
+
app.has_permission('financial.read_all', NULL::uuid) AS read_all_null,
|
|
47919
|
+
app.owner_scoped('financial.movement') AS own_mov,
|
|
47920
|
+
app.owner_scoped('financial.account') AS own_acc,
|
|
47921
|
+
app.grants_exist('financial.movement', ARRAY['shared_with']) AS grants_mov,
|
|
47922
|
+
app.grants_exist('financial.account', ARRAY['shared_with']) AS grants_acc,
|
|
47923
|
+
coalesce(p_from, (current_date - 30)) AS de,
|
|
47924
|
+
coalesce(p_to, current_date) AS ate,
|
|
47925
|
+
app.unit_currency(app.current_tenant_id(), NULL::uuid) AS house
|
|
47926
|
+
),
|
|
47927
|
+
u_read AS (
|
|
47928
|
+
SELECT u FROM ctx, unnest(ctx.units) u
|
|
47929
|
+
WHERE app.has_permission('financial.read', u)
|
|
47930
|
+
),
|
|
47931
|
+
u_read_all AS (
|
|
47932
|
+
SELECT u FROM ctx, unnest(ctx.units) u
|
|
47933
|
+
WHERE app.has_permission('financial.read_all', u)
|
|
47934
|
+
),
|
|
47935
|
+
mov AS NOT MATERIALIZED (
|
|
47936
|
+
SELECT m.*,
|
|
47937
|
+
CASE
|
|
47938
|
+
WHEN m.reverses_id IS NULL THEN
|
|
47939
|
+
(m.external_source IS NULL
|
|
47940
|
+
OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL))
|
|
47941
|
+
ELSE coalesce(
|
|
47942
|
+
(SELECT r.external_source IS NULL
|
|
47943
|
+
OR (r.reconciled_at IS NOT NULL AND r.matched_movement_id IS NULL)
|
|
47944
|
+
FROM public.plg_financial_movements r
|
|
47945
|
+
WHERE r.id = m.reverses_id),
|
|
47946
|
+
(m.external_source IS NULL
|
|
47947
|
+
OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL)))
|
|
47948
|
+
END AS caixa_visivel
|
|
47949
|
+
FROM public.plg_financial_movements m, ctx
|
|
47950
|
+
WHERE m.tenant_id = ctx.tenant
|
|
47951
|
+
AND (CASE WHEN m.unit_id IS NULL THEN ctx.read_null
|
|
47952
|
+
ELSE m.unit_id IN (SELECT u FROM u_read) END)
|
|
47953
|
+
AND (NOT ctx.own_mov
|
|
47954
|
+
OR m.owner_id IS NULL
|
|
47955
|
+
OR m.owner_id = ctx.me
|
|
47956
|
+
OR (ctx.grants_mov
|
|
47957
|
+
AND app.has_grant('financial.movement', m.id, ARRAY['shared_with']))
|
|
47958
|
+
OR (CASE WHEN m.unit_id IS NULL THEN ctx.read_all_null
|
|
47959
|
+
ELSE m.unit_id IN (SELECT u FROM u_read_all) END))
|
|
47960
|
+
),
|
|
47961
|
+
acc AS (
|
|
47962
|
+
SELECT a.id, a.kind, a.currency
|
|
47963
|
+
FROM public.plg_financial_accounts a, ctx
|
|
47964
|
+
WHERE a.tenant_id = ctx.tenant
|
|
47965
|
+
AND a.is_active
|
|
47966
|
+
AND a.kind IN ('bank', 'cash_physical', 'cash_session',
|
|
47967
|
+
'card_receivable_credit', 'card_receivable_debit')
|
|
47968
|
+
AND (CASE WHEN a.unit_id IS NULL THEN ctx.read_null
|
|
47969
|
+
ELSE a.unit_id IN (SELECT u FROM u_read) END)
|
|
47970
|
+
AND (NOT ctx.own_acc
|
|
47971
|
+
OR a.owner_id IS NULL
|
|
47972
|
+
OR a.owner_id = ctx.me
|
|
47973
|
+
OR (ctx.grants_acc
|
|
47974
|
+
AND app.has_grant('financial.account', a.id, ARRAY['shared_with']))
|
|
47975
|
+
OR (CASE WHEN a.unit_id IS NULL THEN ctx.read_all_null
|
|
47976
|
+
ELSE a.unit_id IN (SELECT u FROM u_read_all) END))
|
|
47977
|
+
),
|
|
47978
|
+
-- A balance is in the currency of the ACCOUNT, whatever its movements say.
|
|
47979
|
+
saldo AS (
|
|
47980
|
+
SELECT acc.currency,
|
|
47981
|
+
coalesce(sum(
|
|
47982
|
+
coalesce((SELECT sum(d.amount) FROM mov d
|
|
47983
|
+
WHERE d.debit_account_id = acc.id
|
|
47984
|
+
AND d.status <> 'cancelled'
|
|
47985
|
+
AND d.movement_kind IN ('payment', 'transfer')
|
|
47986
|
+
AND d.caixa_visivel), 0)
|
|
47987
|
+
- coalesce((SELECT sum(c.amount) FROM mov c
|
|
47988
|
+
WHERE c.credit_account_id = acc.id
|
|
47989
|
+
AND c.status <> 'cancelled'
|
|
47990
|
+
AND c.movement_kind IN ('payment', 'transfer')
|
|
47991
|
+
AND c.caixa_visivel), 0)
|
|
47992
|
+
), 0) AS v
|
|
47993
|
+
FROM acc
|
|
47994
|
+
GROUP BY acc.currency
|
|
47995
|
+
),
|
|
47996
|
+
aberto AS (
|
|
47997
|
+
SELECT m.currency,
|
|
47998
|
+
coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,
|
|
47999
|
+
coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,
|
|
48000
|
+
count(*) FILTER (WHERE m.direction = 'credit'
|
|
48001
|
+
AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,
|
|
48002
|
+
coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'
|
|
48003
|
+
AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,
|
|
48004
|
+
count(*) FILTER (WHERE m.direction = 'debit'
|
|
48005
|
+
AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,
|
|
48006
|
+
coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'
|
|
48007
|
+
AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v
|
|
48008
|
+
FROM mov m
|
|
48009
|
+
WHERE m.status IN ('pending', 'partial', 'overdue')
|
|
48010
|
+
GROUP BY m.currency
|
|
48011
|
+
),
|
|
48012
|
+
caixa AS (
|
|
48013
|
+
SELECT m.currency,
|
|
48014
|
+
coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS entrada,
|
|
48015
|
+
coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS saida
|
|
48016
|
+
FROM mov m, ctx
|
|
48017
|
+
WHERE m.movement_kind = 'payment'
|
|
48018
|
+
AND m.payment_date >= ctx.de AND m.payment_date <= ctx.ate
|
|
48019
|
+
AND m.caixa_visivel
|
|
48020
|
+
GROUP BY m.currency
|
|
48021
|
+
),
|
|
48022
|
+
cur AS (
|
|
48023
|
+
SELECT s.currency FROM saldo s
|
|
48024
|
+
UNION SELECT a.currency FROM aberto a
|
|
48025
|
+
UNION SELECT c.currency FROM caixa c
|
|
48026
|
+
UNION SELECT ctx.house FROM ctx
|
|
48027
|
+
)
|
|
48028
|
+
SELECT cur.currency, ctx.house,
|
|
48029
|
+
public.plg_financial_fx_rate(ctx.tenant, cur.currency, ctx.house, current_date),
|
|
48030
|
+
coalesce(saldo.v, 0), coalesce(aberto.receber, 0), coalesce(aberto.pagar, 0),
|
|
48031
|
+
coalesce(caixa.entrada, 0), coalesce(caixa.saida, 0),
|
|
48032
|
+
coalesce(aberto.venc_receber_n, 0)::integer, coalesce(aberto.venc_receber_v, 0),
|
|
48033
|
+
coalesce(aberto.venc_pagar_n, 0)::integer, coalesce(aberto.venc_pagar_v, 0)
|
|
48034
|
+
FROM cur
|
|
48035
|
+
CROSS JOIN ctx
|
|
48036
|
+
LEFT JOIN saldo ON saldo.currency = cur.currency
|
|
48037
|
+
LEFT JOIN aberto ON aberto.currency = cur.currency
|
|
48038
|
+
LEFT JOIN caixa ON caixa.currency = cur.currency
|
|
48039
|
+
ORDER BY (cur.currency = ctx.house) DESC, cur.currency;
|
|
48040
|
+
$function$;
|
|
48041
|
+
REVOKE ALL ON FUNCTION public.financial_summary_by_currency(date, date) FROM PUBLIC, anon;
|
|
48042
|
+
GRANT EXECUTE ON FUNCTION public.financial_summary_by_currency(date, date) TO authenticated, service_role;
|
|
48043
|
+
|
|
48044
|
+
-- Same door, same nine columns: the total in the tenant's own currency.
|
|
48045
|
+
CREATE OR REPLACE FUNCTION public.financial_summary(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)
|
|
48046
|
+
RETURNS TABLE(total_balance numeric, total_receivable numeric, total_payable numeric, monthly_inflow numeric, monthly_outflow numeric, overdue_receivable_count integer, overdue_receivable_amount numeric, overdue_payable_count integer, overdue_payable_amount numeric)
|
|
48047
|
+
LANGUAGE sql
|
|
48048
|
+
STABLE SECURITY DEFINER
|
|
48049
|
+
SET search_path TO ''
|
|
48050
|
+
AS $function$
|
|
48051
|
+
SELECT coalesce(round(sum(s.total_balance * s.fx_rate), 2), 0),
|
|
48052
|
+
coalesce(round(sum(s.total_receivable * s.fx_rate), 2), 0),
|
|
48053
|
+
coalesce(round(sum(s.total_payable * s.fx_rate), 2), 0),
|
|
48054
|
+
coalesce(round(sum(s.monthly_inflow * s.fx_rate), 2), 0),
|
|
48055
|
+
coalesce(round(sum(s.monthly_outflow * s.fx_rate), 2), 0),
|
|
48056
|
+
coalesce(sum(s.overdue_receivable_count), 0)::integer,
|
|
48057
|
+
coalesce(round(sum(s.overdue_receivable_amount * s.fx_rate), 2), 0),
|
|
48058
|
+
coalesce(sum(s.overdue_payable_count), 0)::integer,
|
|
48059
|
+
coalesce(round(sum(s.overdue_payable_amount * s.fx_rate), 2), 0)
|
|
48060
|
+
FROM public.financial_summary_by_currency(p_from, p_to) s;
|
|
48061
|
+
$function$;
|
|
48062
|
+
`;
|
|
48063
|
+
var MIGRATION_126_A_BILL_LINE_CAN_ENTER_STOCK = `-- ============================================================================
|
|
48064
|
+
-- 126 \u2014 a bill line can enter stock
|
|
48065
|
+
--
|
|
48066
|
+
-- A purchase was booked as a bill and the goods never reached stock unless
|
|
48067
|
+
-- someone typed them again on the stock screen. V1 had the gesture on the
|
|
48068
|
+
-- line; this is that gesture.
|
|
48069
|
+
--
|
|
48070
|
+
-- \`financial_invoice_item_receive_stock\` takes ONE line of a payable and
|
|
48071
|
+
-- receives it at a location, at the cost the line was billed at and in the
|
|
48072
|
+
-- bill's currency (the stock side converts to its unit's). A line that names
|
|
48073
|
+
-- no product is linked to one on the way \u2014 linking changes no value, so it is
|
|
48074
|
+
-- allowed on a bill that has already been paid.
|
|
48075
|
+
--
|
|
48076
|
+
-- The receipt is tied to the line, so the same line cannot enter twice, and
|
|
48077
|
+
-- \`financial_invoice_stock_links\` answers "which lines entered" without
|
|
48078
|
+
-- guessing from the invoice number (which \`financial_invoice_stock_entries\`
|
|
48079
|
+
-- still does for notes imported before this).
|
|
48080
|
+
-- ============================================================================
|
|
48081
|
+
|
|
48082
|
+
CREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(
|
|
48083
|
+
p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,
|
|
48084
|
+
p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,
|
|
48085
|
+
p_idempotency_key text DEFAULT NULL::text)
|
|
48086
|
+
RETURNS jsonb
|
|
48087
|
+
LANGUAGE plpgsql
|
|
48088
|
+
SECURITY DEFINER
|
|
48089
|
+
SET search_path TO ''
|
|
48090
|
+
AS $function$
|
|
48091
|
+
DECLARE
|
|
48092
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
48093
|
+
v_item public.items%ROWTYPE;
|
|
48094
|
+
v_inv public.plg_financial_invoices%ROWTYPE;
|
|
48095
|
+
v_product uuid;
|
|
48096
|
+
v_qty numeric;
|
|
48097
|
+
v_cost numeric;
|
|
48098
|
+
v_amend text := coalesce(current_setting('financial.amend', true), '');
|
|
48099
|
+
v_res jsonb;
|
|
48100
|
+
BEGIN
|
|
48101
|
+
SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;
|
|
48102
|
+
IF NOT FOUND THEN
|
|
48103
|
+
RAISE EXCEPTION 'financial_invoice_item_receive_stock: line % not found', p_item_id USING ERRCODE = 'P0002';
|
|
48104
|
+
END IF;
|
|
48105
|
+
SELECT f.* INTO v_inv FROM public.plg_financial_invoices f
|
|
48106
|
+
WHERE f.tenant_id = v_tenant
|
|
48107
|
+
AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))
|
|
48108
|
+
ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;
|
|
48109
|
+
IF NOT FOUND OR v_inv.direction <> 'payable' THEN
|
|
48110
|
+
RAISE EXCEPTION 'S\xF3 a linha de uma conta a pagar d\xE1 entrada no estoque.' USING ERRCODE = '55000';
|
|
48111
|
+
END IF;
|
|
48112
|
+
PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);
|
|
48113
|
+
IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN
|
|
48114
|
+
RAISE EXCEPTION 'Esta linha est\xE1 cancelada.' USING ERRCODE = '55000';
|
|
48115
|
+
END IF;
|
|
48116
|
+
IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN
|
|
48117
|
+
RAISE EXCEPTION 'O estoque n\xE3o est\xE1 instalado nesta conta.' USING ERRCODE = '0A000';
|
|
48118
|
+
END IF;
|
|
48119
|
+
IF EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements m
|
|
48120
|
+
WHERE m.tenant_id = v_tenant AND m.source_item_id = v_item.id AND m.kind = 'in'
|
|
48121
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)) THEN
|
|
48122
|
+
RAISE EXCEPTION 'Esta linha j\xE1 deu entrada no estoque.' USING ERRCODE = '23505';
|
|
48123
|
+
END IF;
|
|
48124
|
+
|
|
48125
|
+
v_product := coalesce(v_item.product_id, p_product_id);
|
|
48126
|
+
IF v_product IS NULL THEN
|
|
48127
|
+
RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';
|
|
48128
|
+
END IF;
|
|
48129
|
+
IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN
|
|
48130
|
+
RAISE EXCEPTION 'Produto n\xE3o encontrado nesta conta.' USING ERRCODE = '22023';
|
|
48131
|
+
END IF;
|
|
48132
|
+
IF v_item.product_id IS NULL THEN
|
|
48133
|
+
-- Naming the product changes no amount, so it passes the paid-bill lock.
|
|
48134
|
+
PERFORM set_config('financial.amend', 'on', true);
|
|
48135
|
+
UPDATE public.items SET product_id = v_product WHERE id = v_item.id;
|
|
48136
|
+
PERFORM set_config('financial.amend', v_amend, true);
|
|
48137
|
+
END IF;
|
|
48138
|
+
|
|
48139
|
+
v_qty := coalesce(p_quantity, v_item.quantity);
|
|
48140
|
+
IF v_qty IS NULL OR v_qty <= 0 THEN
|
|
48141
|
+
RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';
|
|
48142
|
+
END IF;
|
|
48143
|
+
-- What the line cost per unit, discount included.
|
|
48144
|
+
v_cost := round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4);
|
|
48145
|
+
|
|
48146
|
+
EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING
|
|
48147
|
+
p_location_id,
|
|
48148
|
+
jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
|
|
48149
|
+
'product_id', v_product, 'variant_id', v_item.variant_id, 'quantity', v_qty, 'unit_cost', coalesce(v_cost, 0),
|
|
48150
|
+
'source_item_type', 'invoice_item', 'source_item_id', v_item.id))),
|
|
48151
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
48152
|
+
'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,
|
|
48153
|
+
'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),
|
|
48154
|
+
'currency', v_inv.currency, 'fx_rate', p_fx_rate)),
|
|
48155
|
+
coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text);
|
|
48156
|
+
|
|
48157
|
+
PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,
|
|
48158
|
+
jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,
|
|
48159
|
+
'location_id', p_location_id, 'quantity', v_qty, 'unit_cost', v_cost, 'currency', v_inv.currency));
|
|
48160
|
+
RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_qty, 'receipt', v_res);
|
|
48161
|
+
END $function$;
|
|
48162
|
+
REVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) FROM PUBLIC, anon;
|
|
48163
|
+
GRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) TO authenticated, service_role;
|
|
48164
|
+
|
|
48165
|
+
CREATE OR REPLACE FUNCTION public.financial_invoice_stock_links(p_invoice_id uuid)
|
|
48166
|
+
RETURNS TABLE(item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)
|
|
48167
|
+
LANGUAGE plpgsql
|
|
48168
|
+
STABLE SECURITY DEFINER
|
|
48169
|
+
SET search_path TO ''
|
|
48170
|
+
AS $function$
|
|
48171
|
+
DECLARE
|
|
48172
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
48173
|
+
v_inv public.plg_financial_invoices%ROWTYPE;
|
|
48174
|
+
BEGIN
|
|
48175
|
+
SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;
|
|
48176
|
+
IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;
|
|
48177
|
+
PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);
|
|
48178
|
+
IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;
|
|
48179
|
+
|
|
48180
|
+
RETURN QUERY EXECUTE $q$
|
|
48181
|
+
SELECT m.source_item_id, m.product_id, m.source_location_id, sum(m.quantity),
|
|
48182
|
+
greatest(least(sum(m.quantity), coalesce((SELECT sum(sp.quantity) FROM public.plg_inventory_stock_positions sp
|
|
48183
|
+
WHERE sp.tenant_id = $1 AND sp.product_id = m.product_id
|
|
48184
|
+
AND sp.stock_location_id = m.source_location_id), 0)), 0),
|
|
48185
|
+
min(m.movement_date)::date
|
|
48186
|
+
FROM public.plg_inventory_stock_movements m
|
|
48187
|
+
WHERE m.tenant_id = $1 AND m.kind = 'in'
|
|
48188
|
+
AND m.source_item_id IN (SELECT i.id FROM public.items i
|
|
48189
|
+
WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))
|
|
48190
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)
|
|
48191
|
+
GROUP BY m.source_item_id, m.product_id, m.source_location_id
|
|
48192
|
+
$q$ USING v_tenant, p_invoice_id, v_inv.order_id;
|
|
48193
|
+
END $function$;
|
|
48194
|
+
REVOKE ALL ON FUNCTION public.financial_invoice_stock_links(uuid) FROM PUBLIC, anon;
|
|
48195
|
+
GRANT EXECUTE ON FUNCTION public.financial_invoice_stock_links(uuid) TO authenticated, service_role;
|
|
48196
|
+
`;
|
|
48197
|
+
var MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED = `-- ============================================================================
|
|
48198
|
+
-- 127 \u2014 a bill line enters stock as it really arrived
|
|
48199
|
+
--
|
|
48200
|
+
-- 126 received one line once, in the billed quantity, on the default variant.
|
|
48201
|
+
-- Goods do not arrive like the bill reads: 24 shirts billed, 27 delivered,
|
|
48202
|
+
-- some M, some G, some GG, and not always on the same day.
|
|
48203
|
+
--
|
|
48204
|
+
-- \`financial_invoice_item_receive_stock_lines\` takes any number of entries
|
|
48205
|
+
-- (variant + quantity) for one line, and may be called again later for what
|
|
48206
|
+
-- arrives next; each call is one receipt tied to the line. The quantity is
|
|
48207
|
+
-- what was counted, not what was billed. The unit cost is the billed one
|
|
48208
|
+
-- unless the caller says otherwise \u2014 when the supplier sent more for the same
|
|
48209
|
+
-- money, the real cost is lower, and only a person knows that.
|
|
48210
|
+
--
|
|
48211
|
+
-- The single-entry door of 126 stays, as a call to this one.
|
|
48212
|
+
-- ============================================================================
|
|
48213
|
+
|
|
48214
|
+
CREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock_lines(
|
|
48215
|
+
p_item_id uuid, p_location_id uuid, p_lines jsonb,
|
|
48216
|
+
p_product_id uuid DEFAULT NULL::uuid, p_unit_cost numeric DEFAULT NULL::numeric,
|
|
48217
|
+
p_fx_rate numeric DEFAULT NULL::numeric, p_idempotency_key text DEFAULT NULL::text)
|
|
48218
|
+
RETURNS jsonb
|
|
48219
|
+
LANGUAGE plpgsql
|
|
48220
|
+
SECURITY DEFINER
|
|
48221
|
+
SET search_path TO ''
|
|
48222
|
+
AS $function$
|
|
48223
|
+
DECLARE
|
|
48224
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
48225
|
+
v_item public.items%ROWTYPE;
|
|
48226
|
+
v_inv public.plg_financial_invoices%ROWTYPE;
|
|
48227
|
+
v_product uuid;
|
|
48228
|
+
v_cost numeric;
|
|
48229
|
+
v_amend text := coalesce(current_setting('financial.amend', true), '');
|
|
48230
|
+
v_line jsonb;
|
|
48231
|
+
v_qty numeric;
|
|
48232
|
+
v_total numeric := 0;
|
|
48233
|
+
v_variant uuid;
|
|
48234
|
+
v_out jsonb := '[]'::jsonb;
|
|
48235
|
+
v_res jsonb;
|
|
48236
|
+
BEGIN
|
|
48237
|
+
PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);
|
|
48238
|
+
IF p_lines IS NULL OR jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN
|
|
48239
|
+
RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';
|
|
48240
|
+
END IF;
|
|
48241
|
+
SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;
|
|
48242
|
+
IF NOT FOUND THEN
|
|
48243
|
+
RAISE EXCEPTION 'financial_invoice_item_receive_stock_lines: line % not found', p_item_id USING ERRCODE = 'P0002';
|
|
48244
|
+
END IF;
|
|
48245
|
+
SELECT f.* INTO v_inv FROM public.plg_financial_invoices f
|
|
48246
|
+
WHERE f.tenant_id = v_tenant
|
|
48247
|
+
AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))
|
|
48248
|
+
ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;
|
|
48249
|
+
IF NOT FOUND OR v_inv.direction <> 'payable' THEN
|
|
48250
|
+
RAISE EXCEPTION 'S\xF3 a linha de uma conta a pagar d\xE1 entrada no estoque.' USING ERRCODE = '55000';
|
|
48251
|
+
END IF;
|
|
48252
|
+
PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);
|
|
48253
|
+
IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN
|
|
48254
|
+
RAISE EXCEPTION 'Esta linha est\xE1 cancelada.' USING ERRCODE = '55000';
|
|
48255
|
+
END IF;
|
|
48256
|
+
IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN
|
|
48257
|
+
RAISE EXCEPTION 'O estoque n\xE3o est\xE1 instalado nesta conta.' USING ERRCODE = '0A000';
|
|
48258
|
+
END IF;
|
|
48259
|
+
|
|
48260
|
+
v_product := coalesce(v_item.product_id, p_product_id);
|
|
48261
|
+
IF v_product IS NULL THEN
|
|
48262
|
+
RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';
|
|
48263
|
+
END IF;
|
|
48264
|
+
IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN
|
|
48265
|
+
RAISE EXCEPTION 'Produto n\xE3o encontrado nesta conta.' USING ERRCODE = '22023';
|
|
48266
|
+
END IF;
|
|
48267
|
+
IF v_item.product_id IS NULL THEN
|
|
48268
|
+
-- Naming the product changes no amount, so it passes the paid-bill lock.
|
|
48269
|
+
PERFORM set_config('financial.amend', 'on', true);
|
|
48270
|
+
UPDATE public.items SET product_id = v_product WHERE id = v_item.id;
|
|
48271
|
+
PERFORM set_config('financial.amend', v_amend, true);
|
|
48272
|
+
END IF;
|
|
48273
|
+
|
|
48274
|
+
-- The billed unit cost, discount included, unless the caller knows better.
|
|
48275
|
+
v_cost := coalesce(p_unit_cost,
|
|
48276
|
+
round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4), 0);
|
|
48277
|
+
IF v_cost < 0 THEN
|
|
48278
|
+
RAISE EXCEPTION 'O custo n\xE3o pode ser negativo.' USING ERRCODE = '22023';
|
|
48279
|
+
END IF;
|
|
48280
|
+
|
|
48281
|
+
FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines) LOOP
|
|
48282
|
+
BEGIN
|
|
48283
|
+
v_qty := (v_line ->> 'quantity')::numeric;
|
|
48284
|
+
v_variant := nullif(v_line ->> 'variant_id', '')::uuid;
|
|
48285
|
+
EXCEPTION WHEN OTHERS THEN
|
|
48286
|
+
RAISE EXCEPTION 'Entrada inv\xE1lida: %', v_line USING ERRCODE = '22023';
|
|
48287
|
+
END;
|
|
48288
|
+
CONTINUE WHEN v_qty IS NULL OR v_qty = 0;
|
|
48289
|
+
IF v_qty < 0 THEN
|
|
48290
|
+
RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';
|
|
48291
|
+
END IF;
|
|
48292
|
+
v_total := v_total + v_qty;
|
|
48293
|
+
v_out := v_out || jsonb_strip_nulls(jsonb_build_object(
|
|
48294
|
+
'product_id', v_product, 'variant_id', v_variant, 'quantity', v_qty, 'unit_cost', v_cost,
|
|
48295
|
+
'source_item_type', 'invoice_item', 'source_item_id', v_item.id));
|
|
48296
|
+
END LOOP;
|
|
48297
|
+
IF v_total <= 0 THEN
|
|
48298
|
+
RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';
|
|
48299
|
+
END IF;
|
|
48300
|
+
|
|
48301
|
+
EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING
|
|
48302
|
+
p_location_id, v_out,
|
|
48303
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
48304
|
+
'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,
|
|
48305
|
+
'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),
|
|
48306
|
+
'currency', v_inv.currency, 'fx_rate', p_fx_rate)),
|
|
48307
|
+
-- No key from the caller means a new receipt each time: a line can be
|
|
48308
|
+
-- received more than once.
|
|
48309
|
+
coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text || ':' || gen_random_uuid()::text);
|
|
48310
|
+
|
|
48311
|
+
PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,
|
|
48312
|
+
jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,
|
|
48313
|
+
'location_id', p_location_id, 'quantity', v_total, 'unit_cost', v_cost,
|
|
48314
|
+
'currency', v_inv.currency, 'entries', jsonb_array_length(v_out)));
|
|
48315
|
+
RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_total, 'unit_cost', v_cost, 'receipt', v_res);
|
|
48316
|
+
END $function$;
|
|
48317
|
+
REVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) FROM PUBLIC, anon;
|
|
48318
|
+
GRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) TO authenticated, service_role;
|
|
48319
|
+
|
|
48320
|
+
-- 126's door: one entry, in the quantity given or the billed one.
|
|
48321
|
+
CREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(
|
|
48322
|
+
p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,
|
|
48323
|
+
p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,
|
|
48324
|
+
p_idempotency_key text DEFAULT NULL::text)
|
|
48325
|
+
RETURNS jsonb
|
|
48326
|
+
LANGUAGE plpgsql
|
|
48327
|
+
SECURITY DEFINER
|
|
48328
|
+
SET search_path TO ''
|
|
48329
|
+
AS $function$
|
|
48330
|
+
BEGIN
|
|
48331
|
+
-- Same key, same lock as the door it delegates to (re-entrant in one transaction).
|
|
48332
|
+
PERFORM public.plg_financial_claim_idempotency(public.plg_financial_require_tenant(), p_idempotency_key);
|
|
48333
|
+
RETURN public.financial_invoice_item_receive_stock_lines(
|
|
48334
|
+
p_item_id, p_location_id,
|
|
48335
|
+
jsonb_build_array(jsonb_build_object('quantity', coalesce(p_quantity,
|
|
48336
|
+
(SELECT i.quantity FROM public.items i WHERE i.id = p_item_id)))),
|
|
48337
|
+
p_product_id, NULL::numeric, p_fx_rate, p_idempotency_key);
|
|
48338
|
+
END $function$;
|
|
48339
|
+
|
|
48340
|
+
-- Which variant each receipt was for, so the screen can say "10 M, 9 G".
|
|
48341
|
+
-- Dropped first: 128 widens what this returns, and OR REPLACE cannot narrow it back on a replay.
|
|
48342
|
+
DROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);
|
|
48343
|
+
CREATE OR REPLACE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)
|
|
48344
|
+
RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text, location_id uuid,
|
|
48345
|
+
quantity numeric, unit_cost numeric, currency text, entered_on date)
|
|
48346
|
+
LANGUAGE plpgsql
|
|
48347
|
+
STABLE SECURITY DEFINER
|
|
48348
|
+
SET search_path TO ''
|
|
48349
|
+
AS $function$
|
|
48350
|
+
DECLARE
|
|
48351
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
48352
|
+
v_inv public.plg_financial_invoices%ROWTYPE;
|
|
48353
|
+
BEGIN
|
|
48354
|
+
SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;
|
|
48355
|
+
IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;
|
|
48356
|
+
PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);
|
|
48357
|
+
IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;
|
|
48358
|
+
|
|
48359
|
+
RETURN QUERY EXECUTE $q$
|
|
48360
|
+
SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id,
|
|
48361
|
+
sum(m.quantity), max(m.unit_cost), max(m.currency), min(m.movement_date)::date
|
|
48362
|
+
FROM public.plg_inventory_stock_movements m
|
|
48363
|
+
LEFT JOIN public.product_variants v ON v.id = m.variant_id
|
|
48364
|
+
WHERE m.tenant_id = $1 AND m.kind = 'in'
|
|
48365
|
+
AND m.source_item_id IN (SELECT i.id FROM public.items i
|
|
48366
|
+
WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))
|
|
48367
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)
|
|
48368
|
+
GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id
|
|
48369
|
+
ORDER BY v.name NULLS FIRST
|
|
48370
|
+
$q$ USING v_tenant, p_invoice_id, v_inv.order_id;
|
|
48371
|
+
END $function$;
|
|
48372
|
+
REVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;
|
|
48373
|
+
GRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;
|
|
48374
|
+
`;
|
|
48375
|
+
var MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID = `-- ============================================================================
|
|
48376
|
+
-- 128 \u2014 a stock receipt shows what was paid
|
|
48377
|
+
--
|
|
48378
|
+
-- The bill is in reais and the stock is kept in euros. The ledger records both
|
|
48379
|
+
-- sides of every receipt (cost in the stock's currency, and what was paid, in
|
|
48380
|
+
-- which currency, at which rate), but the reading the bill's screen used only
|
|
48381
|
+
-- returned the converted cost \u2014 so the screen could not show the tie it was
|
|
48382
|
+
-- asked to prove.
|
|
48383
|
+
--
|
|
48384
|
+
-- Same reading, with the paid side, the rate and the location's name. The
|
|
48385
|
+
-- return type changes, so the function is dropped first; only the bill's
|
|
48386
|
+
-- screen calls it.
|
|
48387
|
+
-- ============================================================================
|
|
48388
|
+
|
|
48389
|
+
DROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);
|
|
48390
|
+
|
|
48391
|
+
CREATE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)
|
|
48392
|
+
RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text,
|
|
48393
|
+
location_id uuid, location_name text, quantity numeric,
|
|
48394
|
+
unit_cost numeric, currency text,
|
|
48395
|
+
source_unit_cost numeric, source_currency text, fx_rate numeric,
|
|
48396
|
+
entered_on date)
|
|
48397
|
+
LANGUAGE plpgsql
|
|
48398
|
+
STABLE SECURITY DEFINER
|
|
48399
|
+
SET search_path TO ''
|
|
48400
|
+
AS $function$
|
|
48401
|
+
DECLARE
|
|
48402
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
48403
|
+
v_inv public.plg_financial_invoices%ROWTYPE;
|
|
48404
|
+
BEGIN
|
|
48405
|
+
SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;
|
|
48406
|
+
IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;
|
|
48407
|
+
PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);
|
|
48408
|
+
IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;
|
|
48409
|
+
|
|
48410
|
+
RETURN QUERY EXECUTE $q$
|
|
48411
|
+
SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id, l.name,
|
|
48412
|
+
sum(m.quantity), m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate,
|
|
48413
|
+
m.movement_date::date
|
|
48414
|
+
FROM public.plg_inventory_stock_movements m
|
|
48415
|
+
LEFT JOIN public.product_variants v ON v.id = m.variant_id
|
|
48416
|
+
LEFT JOIN public.plg_inventory_stock_locations l ON l.id = m.source_location_id
|
|
48417
|
+
WHERE m.tenant_id = $1 AND m.kind = 'in'
|
|
48418
|
+
AND m.source_item_id IN (SELECT i.id FROM public.items i
|
|
48419
|
+
WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))
|
|
48420
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)
|
|
48421
|
+
GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, v.sort_order, m.source_location_id, l.name,
|
|
48422
|
+
m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate, m.movement_date
|
|
48423
|
+
ORDER BY m.movement_date, v.sort_order NULLS FIRST
|
|
48424
|
+
$q$ USING v_tenant, p_invoice_id, v_inv.order_id;
|
|
48425
|
+
END $function$;
|
|
48426
|
+
REVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;
|
|
48427
|
+
GRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;
|
|
48428
|
+
`;
|
|
48429
|
+
var MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY = `-- ============================================================================
|
|
48430
|
+
-- 129 \u2014 a bill can be written in another currency
|
|
48431
|
+
-- ============================================================================
|
|
48432
|
+
-- An invoice took the currency of its order or of its unit, and nothing else.
|
|
48433
|
+
-- A house that sells in two currencies writes a bill by hand in either one, so
|
|
48434
|
+
-- the door now accepts the choice: \`metadata.currency\` on creation. An order
|
|
48435
|
+
-- still wins \u2014 a bill raised from a sale is in the currency of that sale.
|
|
48436
|
+
-- The choice is made once: an issued invoice's currency stays immutable.
|
|
48437
|
+
-- Idempotent: CREATE OR REPLACE only.
|
|
48438
|
+
|
|
48439
|
+
CREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()
|
|
48440
|
+
RETURNS trigger
|
|
48441
|
+
LANGUAGE plpgsql
|
|
48442
|
+
SECURITY DEFINER
|
|
48443
|
+
SET search_path TO ''
|
|
48444
|
+
AS $function$
|
|
48445
|
+
DECLARE
|
|
48446
|
+
v_asked text := upper(btrim(coalesce(NEW.metadata->>'currency', '')));
|
|
48447
|
+
BEGIN
|
|
48448
|
+
IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
|
|
48449
|
+
IF NEW.order_id IS NOT NULL THEN
|
|
48450
|
+
SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;
|
|
48451
|
+
END IF;
|
|
48452
|
+
IF NEW.currency IS NULL AND v_asked ~ '^[A-Z]{3}$' THEN
|
|
48453
|
+
NEW.currency := v_asked;
|
|
48454
|
+
END IF;
|
|
48455
|
+
NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));
|
|
48456
|
+
END IF;
|
|
48457
|
+
NEW.currency := upper(btrim(NEW.currency));
|
|
48458
|
+
IF NEW.fx_rate IS NULL THEN
|
|
48459
|
+
NEW.fx_rate := public.plg_financial_fx_rate(
|
|
48460
|
+
NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));
|
|
48461
|
+
END IF;
|
|
48462
|
+
RETURN NEW;
|
|
48463
|
+
END $function$;
|
|
48464
|
+
`;
|
|
48465
|
+
var MIGRATION_130_A_BILL_CAN_BE_TAGGED = `-- ============================================================================
|
|
48466
|
+
-- 130 \u2014 a bill can be tagged
|
|
48467
|
+
-- ============================================================================
|
|
48468
|
+
-- Every record of the core has \`tags\` since spine 150: how THIS house slices
|
|
48469
|
+
-- its records, for a purpose the product did not foresee. Bills were the one
|
|
48470
|
+
-- place the operator looked for the tag and did not find it. Same shape as
|
|
48471
|
+
-- there \u2014 \`text[]\` read and written with the row, a GIN index for "who has
|
|
48472
|
+
-- this one". Tags are not part of what an issued invoice freezes.
|
|
48473
|
+
-- Idempotent: IF NOT EXISTS throughout.
|
|
48474
|
+
|
|
48475
|
+
ALTER TABLE public.plg_financial_invoices
|
|
48476
|
+
ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
|
|
48477
|
+
|
|
48478
|
+
COMMENT ON COLUMN public.plg_financial_invoices.tags IS 'Free labels the house puts on a bill to slice its own lists.';
|
|
48479
|
+
|
|
48480
|
+
CREATE INDEX IF NOT EXISTS plg_financial_invoices_tags_idx
|
|
48481
|
+
ON public.plg_financial_invoices USING gin (tags);
|
|
48482
|
+
`;
|
|
46498
48483
|
var MIGRATIONS = [
|
|
46499
48484
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
46500
48485
|
{ id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
|
|
@@ -46618,7 +48603,15 @@ var MIGRATIONS = [
|
|
|
46618
48603
|
{ id: "119_an_unpaid_legacy_bill_can_be_rescheduled", sql: MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED },
|
|
46619
48604
|
{ id: "120_cancelar_e_por_item_com_motivo_e_destino", sql: MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO },
|
|
46620
48605
|
{ id: "121_o_motivo_e_do_estabelecimento", sql: MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO },
|
|
46621
|
-
{ id: "122_o_cancelamento_fecha_as_portas_que_abriu", sql: MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU }
|
|
48606
|
+
{ id: "122_o_cancelamento_fecha_as_portas_que_abriu", sql: MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU },
|
|
48607
|
+
{ id: "123_the_mirror_does_not_bill_a_recurrence_twice", sql: MIGRATION_123_THE_MIRROR_DOES_NOT_BILL_A_RECURRENCE_TWICE },
|
|
48608
|
+
{ id: "124_money_knows_its_currency", sql: MIGRATION_124_MONEY_KNOWS_ITS_CURRENCY },
|
|
48609
|
+
{ id: "125_the_summary_adds_one_currency_at_a_time", sql: MIGRATION_125_THE_SUMMARY_ADDS_ONE_CURRENCY_AT_A_TIME },
|
|
48610
|
+
{ id: "126_a_bill_line_can_enter_stock", sql: MIGRATION_126_A_BILL_LINE_CAN_ENTER_STOCK },
|
|
48611
|
+
{ id: "127_a_bill_line_enters_stock_as_it_really_arrived", sql: MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED },
|
|
48612
|
+
{ id: "128_a_stock_receipt_shows_what_was_paid", sql: MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID },
|
|
48613
|
+
{ 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 }
|
|
46622
48615
|
];
|
|
46623
48616
|
var STANDALONE_CONFIG = resolveConfig();
|
|
46624
48617
|
registerTranslations(financialLocales);
|
|
@@ -46628,8 +48621,8 @@ function ReceivePaymentModal({
|
|
|
46628
48621
|
onClose,
|
|
46629
48622
|
onPaid
|
|
46630
48623
|
}) {
|
|
46631
|
-
const provider =
|
|
46632
|
-
const store6 =
|
|
48624
|
+
const provider = React29__default.useMemo(() => createSafeFinancialProvider(), []);
|
|
48625
|
+
const store6 = React29__default.useMemo(() => createFinancialStore(provider), [provider]);
|
|
46633
48626
|
return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
|
|
46634
48627
|
PaymentModal,
|
|
46635
48628
|
{
|
|
@@ -46647,7 +48640,7 @@ function createFinancialSettingsPlugin(options) {
|
|
|
46647
48640
|
...financialRegistries.filter((r) => RECEIVING_REGISTRIES.includes(r.id)),
|
|
46648
48641
|
...options?.extraRegistries ?? []
|
|
46649
48642
|
];
|
|
46650
|
-
const SettingsTab = () =>
|
|
48643
|
+
const SettingsTab = () => React29__default.createElement(PluginSettingsPanel, {
|
|
46651
48644
|
title: "Pagamentos",
|
|
46652
48645
|
registries,
|
|
46653
48646
|
hostPluginId: "financial",
|
|
@@ -46704,27 +48697,27 @@ function ClientAccountPanel({
|
|
|
46704
48697
|
currency = BRL
|
|
46705
48698
|
}) {
|
|
46706
48699
|
const provider = useFinancialProvider();
|
|
46707
|
-
const [account, setAccount] =
|
|
46708
|
-
const [busy, setBusy] =
|
|
46709
|
-
const [viewInvoiceId, setViewInvoiceId] =
|
|
46710
|
-
const [comprando, setComprando] =
|
|
46711
|
-
const [buscaServico, setBuscaServico] =
|
|
46712
|
-
const [catalogo, setCatalogo] =
|
|
46713
|
-
const [escolhidos, setEscolhidos] =
|
|
46714
|
-
const [comprandoBusy, setComprandoBusy] =
|
|
46715
|
-
const [erroCompra, setErroCompra] =
|
|
46716
|
-
const load =
|
|
48700
|
+
const [account, setAccount] = React29__default.useState(null);
|
|
48701
|
+
const [busy, setBusy] = React29__default.useState(false);
|
|
48702
|
+
const [viewInvoiceId, setViewInvoiceId] = React29__default.useState(null);
|
|
48703
|
+
const [comprando, setComprando] = React29__default.useState(false);
|
|
48704
|
+
const [buscaServico, setBuscaServico] = React29__default.useState("");
|
|
48705
|
+
const [catalogo, setCatalogo] = React29__default.useState([]);
|
|
48706
|
+
const [escolhidos, setEscolhidos] = React29__default.useState([]);
|
|
48707
|
+
const [comprandoBusy, setComprandoBusy] = React29__default.useState(false);
|
|
48708
|
+
const [erroCompra, setErroCompra] = React29__default.useState(null);
|
|
48709
|
+
const load = React29__default.useCallback(async () => {
|
|
46717
48710
|
if (!personId) return;
|
|
46718
48711
|
setAccount(await provider.getClientAccount(personId));
|
|
46719
48712
|
}, [personId, provider]);
|
|
46720
|
-
|
|
48713
|
+
React29__default.useEffect(() => {
|
|
46721
48714
|
void load();
|
|
46722
48715
|
}, [load]);
|
|
46723
48716
|
const viewInvoice = account?.invoices.find((i) => i.id === viewInvoiceId) ?? null;
|
|
46724
48717
|
const viewEditable = invoiceOpensForEditing(viewInvoice?.status);
|
|
46725
48718
|
const forThis = account?.invoices.filter((i) => orderId && i.orderId === orderId) ?? [];
|
|
46726
48719
|
const hasForThis = forThis.length > 0;
|
|
46727
|
-
|
|
48720
|
+
React29__default.useEffect(() => {
|
|
46728
48721
|
if (!comprando) return;
|
|
46729
48722
|
let vivo = true;
|
|
46730
48723
|
const t2 = setTimeout(async () => {
|
|
@@ -46999,8 +48992,8 @@ function ClientAccountPanel({
|
|
|
46999
48992
|
] });
|
|
47000
48993
|
}
|
|
47001
48994
|
function ClientAccountInline(props) {
|
|
47002
|
-
const provider =
|
|
47003
|
-
const store6 =
|
|
48995
|
+
const provider = React29__default.useMemo(() => createSafeFinancialProvider(), []);
|
|
48996
|
+
const store6 = React29__default.useMemo(() => createFinancialStore(provider), [provider]);
|
|
47004
48997
|
return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(ClientAccountPanel, { ...props }) });
|
|
47005
48998
|
}
|
|
47006
48999
|
function ClientAccountModal({
|
|
@@ -47048,11 +49041,11 @@ function createFinancialPlugin(options) {
|
|
|
47048
49041
|
const settingsRegistries = registries.filter((r) => r.id !== "bank-accounts");
|
|
47049
49042
|
const AccountsRegistryPage = createCrudPage(financialAccountEntity, { feature: "financial" });
|
|
47050
49043
|
AccountsRegistryPage.displayName = "FinancialAccountsRegistryPage";
|
|
47051
|
-
const PersonStatementBound = (props) =>
|
|
49044
|
+
const PersonStatementBound = (props) => React29__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
47052
49045
|
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
47053
|
-
const SupplierFinancialSettingsBound = (props) =>
|
|
49046
|
+
const SupplierFinancialSettingsBound = (props) => React29__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
|
|
47054
49047
|
SupplierFinancialSettingsBound.displayName = "FinancialSupplierSettings";
|
|
47055
|
-
const PageComponent = () =>
|
|
49048
|
+
const PageComponent = () => React29__default.createElement(FinancialPage, { config, provider, store: store6, registries });
|
|
47056
49049
|
const AgentMount = createFinancialAgentMount({ config, provider });
|
|
47057
49050
|
return {
|
|
47058
49051
|
id: "financial",
|
|
@@ -47110,7 +49103,7 @@ function createFinancialPlugin(options) {
|
|
|
47110
49103
|
// tem — lá é um ícone no cabeçalho, de qualquer tela.
|
|
47111
49104
|
headerControls: config.modules.cashRegisters ? [{
|
|
47112
49105
|
id: "financial.cash-session",
|
|
47113
|
-
component: (() =>
|
|
49106
|
+
component: (() => React29__default.createElement(CashSessionHeaderControl, { config, provider, store: store6 })),
|
|
47114
49107
|
order: 90,
|
|
47115
49108
|
permission: { feature: "financial", action: "read" }
|
|
47116
49109
|
}] : [],
|
|
@@ -47341,9 +49334,9 @@ function createFinancialPlugin(options) {
|
|
|
47341
49334
|
// lado, e a corrida de "Plugins" não é partida ao meio.
|
|
47342
49335
|
group: "Financeiro",
|
|
47343
49336
|
component: (() => {
|
|
47344
|
-
const FinancialSettingsTab = () =>
|
|
49337
|
+
const FinancialSettingsTab = () => React29__default.createElement(PluginSettingsPanel, {
|
|
47345
49338
|
title: "Financial Settings",
|
|
47346
|
-
generalSettings:
|
|
49339
|
+
generalSettings: React29__default.createElement(FinancialGeneralSettings, { config }),
|
|
47347
49340
|
registries: settingsRegistries,
|
|
47348
49341
|
hostPluginId: "financial",
|
|
47349
49342
|
routeBase: "/settings/financial"
|
|
@@ -47368,7 +49361,7 @@ function createFinancialPlugin(options) {
|
|
|
47368
49361
|
icon: "HandCoins",
|
|
47369
49362
|
group: "Financeiro",
|
|
47370
49363
|
component: (() => {
|
|
47371
|
-
const CommissionRulesTab = () =>
|
|
49364
|
+
const CommissionRulesTab = () => React29__default.createElement(CommissionRulesView, { provider });
|
|
47372
49365
|
CommissionRulesTab.displayName = "CommissionRulesTab";
|
|
47373
49366
|
return CommissionRulesTab;
|
|
47374
49367
|
})(),
|