@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.
Files changed (37) hide show
  1. package/dist/components/CancelInvoiceDialog.d.ts +15 -0
  2. package/dist/components/CancelInvoiceDialog.d.ts.map +1 -1
  3. package/dist/components/CashSessionHeaderControl.d.ts +2 -1
  4. package/dist/components/CashSessionHeaderControl.d.ts.map +1 -1
  5. package/dist/components/FinancialGeneralSettings.d.ts +2 -0
  6. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -1
  7. package/dist/components/InstallmentRow.d.ts +4 -1
  8. package/dist/components/InstallmentRow.d.ts.map +1 -1
  9. package/dist/components/PaymentReceiptModal.d.ts.map +1 -1
  10. package/dist/components/PersonStatementWidget.d.ts.map +1 -1
  11. package/dist/components/ReceiveStockDialog.d.ts +22 -0
  12. package/dist/components/ReceiveStockDialog.d.ts.map +1 -0
  13. package/dist/components/StockEntryNote.d.ts +29 -0
  14. package/dist/components/StockEntryNote.d.ts.map +1 -0
  15. package/dist/data/supabase.d.ts.map +1 -1
  16. package/dist/data/types.d.ts +56 -0
  17. package/dist/data/types.d.ts.map +1 -1
  18. package/dist/index.js +2691 -698
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/fx-auto.d.ts +7 -0
  21. package/dist/lib/fx-auto.d.ts.map +1 -0
  22. package/dist/lib/installment-amounts.d.ts +18 -0
  23. package/dist/lib/installment-amounts.d.ts.map +1 -0
  24. package/dist/lib/item-icons.d.ts +6 -0
  25. package/dist/lib/item-icons.d.ts.map +1 -0
  26. package/dist/locales/en.d.ts.map +1 -1
  27. package/dist/locales/pt-BR.d.ts.map +1 -1
  28. package/dist/migrations/index.d.ts +9 -1
  29. package/dist/migrations/index.d.ts.map +1 -1
  30. package/dist/types.d.ts +21 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/views/FinancialPage.d.ts.map +1 -1
  33. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  34. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  35. package/dist/views/InvoiceListView.d.ts.map +1 -1
  36. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  37. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import * as React26 from 'react';
2
- import React26__default, { useState, useEffect, useMemo, useRef, 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, 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, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, Card, CardContent, Skeleton, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MultiSelect, PageTransition, ModulePage, Badge, TooltipProvider, ConfirmDialog, CurrencyInput, Input, PageHeaderActions, ChoiceSelect, ModalFooter, Checkbox, SegmentedControl, Hint, PageTabs, Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverTrigger, PopoverContent, openPrintWindow, printDocument, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, RequiredFilter, FilterMenuRow, ChoiceList, ModalFlow, Switch, WizardSteps, KpiCard, ChartWidget, CardHeader, CardTitle, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
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, resolveEntityRoute, PermissionGate, createOneShotAsk, askAI, requestChatUpload, toInlineFile, shrinkForVision } from '@fayz-ai/admin';
7
- import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, Receipt, Gift, Briefcase, Package, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Paperclip, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, Image, UploadCloud, Calendar, User, Unlink, ChevronDown, Eye, Inbox, Clock, ClipboardCheck, BookOpen, Info, FolderOpen, Folder, RotateCcw, Settings, ArrowLeft, ChevronLeft, 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';
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 cents = centsAt >= 0 ? sumGroup(words.slice(0, centsAt).slice(lastCurrencyIndex(words, centsAt))) : 0;
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 && cents > 0 && cents < 100 ? total - cents + cents / 100 : total;
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 = React26.useRef(onOpenDraft);
911
+ const live = React29.useRef(onOpenDraft);
912
912
  live.current = onOpenDraft;
913
- const contribution = React26.useMemo(() => ({
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] = React26__default.useState(0);
1408
- const [bankAccountId, setBankAccountId] = React26__default.useState("");
1409
- const [draft, setDraft] = React26__default.useState(null);
1410
- const [context, setContext] = React26__default.useState(null);
1411
- const [companyName, setCompanyName] = React26__default.useState("");
1412
- const [companyDocument, setCompanyDocument] = React26__default.useState("");
1413
- const [lines, setLines] = React26__default.useState([]);
1414
- const [companyUnsaved, setCompanyUnsaved] = React26__default.useState(false);
1415
- const [loadingContext, setLoadingContext] = React26__default.useState(false);
1416
- const [busy, setBusy] = React26__default.useState(false);
1417
- const [error, setError] = React26__default.useState("");
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 = React26__default.useMemo(() => lines.map((line) => ({
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 = React26__default.useMemo(() => cnabPayeeGaps(items), [items]);
1448
- const batches = React26__default.useMemo(() => {
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 = React26.useCallback(async (query) => {
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 summary = useMemo(() => {
2433
- let totalAmount = 0;
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
- totalAmount += inv.totalAmount;
2439
- paidAmount += inv.paidAmount;
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
- openAmount2 = totalAmount - paidAmount;
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 [bulkBusy, setBulkBusy] = useState(false);
2447
- const [cancelReason, setCancelReason] = useState("");
2448
- const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-bulk-cancel"));
2449
- const closeBulkCancel = useCallback(() => {
2450
- setCancelTarget(null);
2451
- setCancelReason("");
2452
- cancelRequestKey.current = createIdempotencyKey("financial:invoice-bulk-cancel");
2453
- }, []);
2454
- const confirmBulkCancel = useCallback(async () => {
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
- setBulkBusy(false);
2473
- closeBulkCancel();
2823
+ return [...byInvoice.values()];
2824
+ }, [cancelTarget]);
2825
+ const afterBulkCancel = useCallback(async () => {
2826
+ setCancelTarget(null);
2474
2827
  await Promise.all([fetchInvoices(query), fetchSummary()]);
2475
- if (done > 0) toast.success(t2("financial.invoice.bulkCancelDone", { count: String(done) }));
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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, bulkBusy, direction, exportSelected, printSelected, t2]);
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 && createPortal(
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 = React26.useRef(draft);
4279
+ const live = React29.useRef(draft);
3959
4280
  live.current = draft;
3960
- const lookup = React26.useMemo(
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
- React26.useEffect(() => {
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 = React26.useMemo(() => ({
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 = React26.useMemo(() => ({
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 = React26__default.useRef(createPendingCommand(() => createIdempotencyKey("financial:payment"), (input) => input.movementId));
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
- React26__default.useEffect(() => {
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] = React26__default.useState(false);
4783
- const [q, setQ] = React26__default.useState("");
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
- var DESTINATIONS = [
4968
- { value: "devolver", label: "Devolver ao fornecedor" },
4969
- { value: "perda", label: "Baixar como perda" },
4970
- { value: "manter", label: "Manter no estoque" }
4971
- ];
4972
- function day(iso) {
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 CancelInvoiceDialog({ invoiceId, counterpartyName, totalAmount, items, preselectItemId, onClose, onCancelled }) {
5384
+ function StockEntryDetailsDialog({ title, entries, onClose }) {
4977
5385
  const { currency } = useFinancialConfig();
4978
- const provider = useFinancialProvider();
4979
- const [selected, setSelected] = useState(
4980
- () => new Set(preselectItemId ? [preselectItemId] : items.map((i) => i.id))
4981
- );
4982
- const [reasons, setReasons] = useState(null);
4983
- const [reasonId, setReasonId] = useState();
4984
- const [detail, setDetail] = useState("");
4985
- const [entries, setEntries] = useState(null);
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 text-destructive", children: [
5067
- /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4" }),
5068
- " Cancelar"
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__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
5074
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
5075
- "Selecione o que cancelar. Pagamentos j\xE1 alocados ao que for cancelado ser\xE3o liberados como cr\xE9dito",
5076
- counterpartyName ? ` de ${counterpartyName}` : "",
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
- 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: [
5080
- /* @__PURE__ */ jsx(
5081
- Checkbox,
5082
- {
5083
- checked: whole,
5084
- indeterminate: !whole && selected.size > 0,
5085
- color: "destructive",
5086
- onChange: (on) => setSelected(on ? new Set(items.map((i) => i.id)) : /* @__PURE__ */ new Set())
5087
- }
5088
- ),
5089
- "Cancelar a conta inteira"
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__ */ 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: [
5092
- /* @__PURE__ */ jsx(Checkbox, { checked: selected.has(it.id), onChange: (on) => toggle(it.id, on) }),
5093
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: it.description }),
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 items-center justify-between border-t pt-3 text-sm font-medium", children: [
5135
- /* @__PURE__ */ jsx("span", { children: whole ? "Conta inteira ser\xE1 cancelada" : "Total a cancelar" }),
5136
- /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCurrency(amount, currency) })
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: "space-y-2", children: [
5139
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Motivo do cancelamento" }),
5140
- reasons === null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }) : /* @__PURE__ */ jsx(
5141
- ChoiceSelect,
5142
- {
5143
- "aria-label": "Motivo do cancelamento",
5144
- options: reasons.map((r) => ({ value: r.id, label: r.name })),
5145
- value: reasonId,
5146
- onChange: setReasonId,
5147
- placeholder: "Selecione um motivo..."
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] = React26__default.useState(null);
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
- React26__default.useEffect(() => {
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 React26__default.useMemo(
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 { currency, itemTypes: itemTypesRaw = [], locations = [] } = config;
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 = React26__default.useMemo(
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 = React26__default.useMemo(
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 = React26__default.useCallback(() => {
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 invoiceMovements = movements.data.filter((m) => m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0));
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 = React26__default.useMemo(
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 = React26__default.useMemo(() => {
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 total = ordenados.reduce((soma, m) => soma + (ehEstorno(m) ? -1 : 1) * (m.paidAmount || m.amount), 0);
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
- ordenados.length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-card bg-muted/60 px-4 py-2", children: [
6600
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t2("financial.paymentReceipt.count", { count: ordenados.length }) }),
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
- ordenados.map((payment, indice) => {
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
- ordenados.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 }) }),
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
- ordenados.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: t2("financial.paymentReceipt.empty") })
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 ITEM_ICONS = {
6656
- product: Package,
6657
- service: Briefcase,
6658
- gift_card: Gift,
6659
- other: Receipt
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] = React26__default.useState(/* @__PURE__ */ new Map());
6812
- React26__default.useEffect(() => {
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
- { id: "cancel", label: "Cancelar", icon: Ban, onSelect: () => setCancelTarget(it.id) }
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__ */ jsxs(OrderDetailBlock, { title: t2("financial.invoice.paymentSchedule"), children: [
6957
- attachments.length > 0 && /* @__PURE__ */ jsxs(
6958
- "button",
6959
- {
6960
- type: "button",
6961
- onClick: () => setAttachmentsOpen(true),
6962
- title: attachments.map((a) => a.fileName).join("\n"),
6963
- className: "mb-2 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",
6964
- children: [
6965
- /* @__PURE__ */ jsx(Paperclip, { className: "h-3.5 w-3.5", "aria-hidden": true }),
6966
- attachments.length === 1 ? "Ver o anexo" : `Ver os ${attachments.length} anexos`
6967
- ]
6968
- }
6969
- ),
6970
- /* @__PURE__ */ jsxs("div", { className: "-mx-4 -mb-4", children: [
6971
- 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: movements.map((mov) => {
6972
- const remaining = mov.amount - mov.paidAmount;
6973
- const isPaid2 = mov.status === "paid";
6974
- const isPartial = mov.status === "partial";
6975
- const hasPaidDetails = isPaid2 || isPartial;
6976
- const instPayments = settlements.byBillId.get(mov.id) ?? [];
6977
- const hows = instPayments.map((payment) => describePayment(payment, howCtx));
6978
- const paymentSummary = [...new Set(hows.map((how2) => [how2.method, how2.account?.name].filter(Boolean).join(" \xB7 ")).filter(Boolean))].join(" / ");
6979
- const how = hows[0];
6980
- const howCard = how?.account?.accountType === "card_payable" ? how.account : void 0;
6981
- const HowIcon = METHOD_ICONS[how?.typeKind ?? ""] ?? (how ? CreditCard : null);
6982
- const srcMeta = mov.metadata ?? {};
6983
- const srcStr = (v) => typeof v === "string" && v ? v : void 0;
6984
- const srcTypeId = srcStr(srcMeta.payMethodTypeId);
6985
- const srcAccountId = srcStr(srcMeta.payAccountId);
6986
- const editavel = !hasPaidDetails && invoice.status !== "cancelled";
6987
- const canChooseSource = editavel && Math.abs(mov.amount) >= 5e-3;
6988
- const faltaParaPagar = !srcTypeId ? t2("financial.payment.methodRequired") : !srcAccountId ? t2("financial.payment.accountRequired") : null;
6989
- const ativa = invoice.status !== "cancelled";
6990
- return /* @__PURE__ */ jsx(
6991
- InstallmentRow,
6992
- {
6993
- number: mov.installmentNumber ?? 1,
6994
- amount: formatCurrency(mov.amount, currency),
6995
- status: mov.status,
6996
- dueDate: mov.dueDate,
6997
- onDueDateChange: editavel ? (v) => {
6998
- void patchDueDate(mov, v);
6999
- } : void 0,
7000
- dueExtra: /* @__PURE__ */ jsxs(Fragment, { children: [
7001
- mov.paidAmount > 0 && !isPaid2 && ` \xB7 ${t2("financial.invoice.paidLabel")} ${formatCurrency(mov.paidAmount, currency)}`,
7002
- mov.paymentDate && ` \xB7 ${t2("financial.invoice.paidOn")} ${mov.paymentDate}`
7003
- ] }),
7004
- onClick: hasPaidDetails ? () => setReceiptBill(mov) : void 0,
7005
- source: canChooseSource ? /* @__PURE__ */ jsx(
7006
- InstallmentSourceFields,
7007
- {
7008
- direction,
7009
- value: { payMethodTypeId: srcTypeId, payAccountId: srcAccountId },
7010
- onChange: (next) => {
7011
- void patchSource(mov, {
7012
- payMethodTypeId: next.payMethodTypeId ?? null,
7013
- payAccountId: next.payAccountId ?? null
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
- onClick: () => setPayingMovement(mov),
7045
- disabled: !!faltaParaPagar,
7046
- title: faltaParaPagar ?? void 0,
7047
- children: [
7048
- t2("financial.invoice.pay"),
7049
- " ",
7050
- remaining < mov.amount ? formatCurrency(remaining, currency) : ""
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
- isPaid2 && reversiblePayments(instPayments).length > 0 && ativa && /* @__PURE__ */ jsx(InstallmentActionButton, { icon: Undo2, onClick: () => setReversing(reversiblePayments(instPayments).at(-1)), children: t2("financial.invoice.reverse") }),
7055
- isPartial && ativa && /* @__PURE__ */ jsxs(InstallmentPayButton, { variant: "outline", onClick: () => setPayingMovement(mov), children: [
7056
- t2("financial.invoice.pay"),
7057
- " ",
7058
- formatCurrency(remaining, currency)
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
- mov.id
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 qty = counts[d.value] ?? 0;
7449
- const subtotal = qty * d.value;
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: qty || "",
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 = React26__default.useCallback(async () => {
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 = React26__default.useMemo(() => [{
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] = React26__default.useState(firstOfMonth());
8595
- const [to, setTo] = React26__default.useState(today2());
8596
- const [preset, setPreset] = React26__default.useState("this_month");
8597
- const [running, setRunning] = React26__default.useState(false);
8598
- const [lastRun, setLastRun] = React26__default.useState(null);
8599
- const [generation, setGeneration] = React26__default.useState(null);
8600
- const [busy, setBusy] = React26__default.useState(null);
8601
- const [error, setError] = React26__default.useState(null);
8602
- const loadGeneration = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
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 = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => [{
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] = React26__default.useState([]);
9116
- const [unitId, setUnitId] = React26__default.useState("");
9117
- const [year, setYear] = React26__default.useState((/* @__PURE__ */ new Date()).getFullYear());
9118
- const [rows, setRows] = React26__default.useState(null);
9119
- const [editing, setEditing] = React26__default.useState(null);
9120
- const [error, setError] = React26__default.useState(null);
9121
- React26__default.useEffect(() => {
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 = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
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] = React26__default.useState(String(goal.targetAmount || ""));
9272
- const [sup, setSup] = React26__default.useState(String(goal.superAmount || ""));
9273
- const [hyper, setHyper] = React26__default.useState(String(goal.hyperAmount || ""));
9274
- const [saving, setSaving] = React26__default.useState(false);
9275
- const [error, setError] = React26__default.useState(null);
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] = React26__default.useState(null);
9367
- const [cards, setCards] = React26__default.useState(null);
9368
- const [error, setError] = React26__default.useState(null);
9369
- const [issuing, setIssuing] = React26__default.useState(false);
9370
- const [issued, setIssued] = React26__default.useState(null);
9371
- const [activating, setActivating] = React26__default.useState(null);
9372
- const [search, setSearch] = React26__default.useState("");
9373
- const [state, setState] = React26__default.useState("");
9374
- const [minValue, setMinValue] = React26__default.useState("");
9375
- const [maxValue, setMaxValue] = React26__default.useState("");
9376
- const [from, setFrom] = React26__default.useState("");
9377
- const [to, setTo] = React26__default.useState("");
9378
- const money4 = React26__default.useCallback((v) => formatCurrency(v, currency), [currency]);
9379
- const seq = React26__default.useRef(0);
9380
- const load = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
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
- React26__default.useEffect(() => {
10172
+ React29__default.useEffect(() => {
9411
10173
  if (enabled) void load();
9412
10174
  }, [enabled, load]);
9413
- const columns = React26__default.useMemo(() => [
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] = React26__default.useState("10");
9595
- const [amount, setAmount] = React26__default.useState("100");
9596
- const [expiresAt, setExpiresAt] = React26__default.useState("");
9597
- const [note, setNote] = React26__default.useState("");
9598
- const [saving, setSaving] = React26__default.useState(false);
9599
- const [error, setError] = React26__default.useState(null);
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, day2] = value.split("-");
9698
- return year && month && day2 ? `${day2}/${month}/${year}` : value;
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 num2 = (v, fallback = 0) => {
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 = num2(item.quantity, 1) || 1;
10002
- const unitPrice = item.unitPrice != null ? num2(item.unitPrice) : num2(item.total) / quantity;
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, day2] = value.split("-");
10024
- return year && month && day2 ? `${day2}/${month}/${year}` : value;
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, day2] = value.split("-");
10986
- return year && month && day2 ? `${day2}/${month}/${year}` : value;
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 = React26__default.useMemo(
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 = React26__default.useMemo(() => [{
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, day2] = value.split("-");
11566
- return year && month && day2 ? `${day2}/${month}/${year}` : value;
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] = React26__default.useState(null);
11594
- const [search, setSearch] = React26__default.useState("");
11595
- const [status, setStatus] = React26__default.useState("all");
11596
- const [changing, setChanging] = React26__default.useState(null);
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 = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
12370
+ React29__default.useEffect(() => {
11609
12371
  void load();
11610
12372
  }, [load]);
11611
- const filtered = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => [
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] = React26__default.useState(null);
11741
- const [loaded, setLoaded] = React26__default.useState(!valueId);
11742
- const [contact, setContact] = React26__default.useState(null);
11743
- const [counterpartyAccounting, setCounterpartyAccounting] = React26__default.useState({
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 = React26__default.useRef(0);
11749
- const counterpartyLookup = React26__default.useMemo(
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 = React26__default.useRef(newRecurringItem());
11757
- const [items, setItems] = React26__default.useState([initialItem.current]);
11758
- const [expandedItemId, setExpandedItemId] = React26__default.useState(initialItem.current._id);
11759
- const [addingItem, setAddingItem] = React26__default.useState(false);
11760
- const [frequency, setFrequency] = React26__default.useState("monthly");
11761
- const [dueDay, setDueDay] = React26__default.useState("1");
11762
- const [startsOn, setStartsOn] = React26__default.useState(today3());
11763
- const [endsOn, setEndsOn] = React26__default.useState("");
11764
- const [unitId, setUnitId] = React26__default.useState(locations[0]?.id ?? "");
11765
- const [availableLocations, setAvailableLocations] = React26__default.useState(locations);
11766
- const [notes, setNotes] = React26__default.useState("");
11767
- const [isActive, setIsActive] = React26__default.useState(true);
11768
- const [accounts, setAccounts] = React26__default.useState([]);
11769
- const [costCenters, setCostCenters] = React26__default.useState([]);
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] = React26__default.useState(false);
11772
- const [ending, setEnding] = React26__default.useState(false);
11773
- const [error, setError] = React26__default.useState(null);
11774
- React26__default.useEffect(() => {
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
- React26__default.useEffect(() => {
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 = React26__default.useMemo(() => configuredItemTypes.map((tp) => {
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 = React26__default.useMemo(() => availableInvoiceItemTypes(
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 = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => items.reduce(
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 = React26__default.useRef(null);
11863
- React26__default.useEffect(() => {
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__ */ jsx(SettingsGroup, { title: t2("financial.settings.modules"), description: t2("financial.settings.modulesDesc"), children: /* @__PURE__ */ jsx(
12141
- ToggleRow,
12142
- {
12143
- label: t2("financial.settings.commissions"),
12144
- description: t2("financial.settings.commissionsDesc"),
12145
- checked: mods.get("commissions"),
12146
- onChange: (v) => mods.set("commissions", v)
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
- () => appConfig.modules.commissions && commissionsOn ? appConfig : { ...appConfig, modules: { ...appConfig.modules, commissions: false } },
12376
- [appConfig, commissionsOn]
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
- return /* @__PURE__ */ jsx(KpiCard, { label: t2("financial.summary.totalBalance"), icon: "Wallet", value: formatCurrency(summary?.totalBalance ?? 0, currency), sub: loading ? void 0 : t2("financial.summary.allAccounts") });
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] = React26__default.useState(false);
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] = React26__default.useState([]);
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] = React26__default.useState([]);
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
- return data ? rowToInvoice(data) : null;
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: i.metadata?.itemKind ?? "other",
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 ?? i.metadata?.referenceId,
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: i.metadata ?? i.itemMetadata ?? void 0,
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: { contactName: input.contactName, ...input.fiscalNumber ? { fiscalNumber: input.fiscalNumber } : {} },
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 { data, error } = await pub.rpc("financial_invoice_stock_entries", { p_invoice_id: invoiceId });
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 { data, error } = await pub.rpc("financial_summary", {
16151
- p_from: dateRange?.from ?? null,
16152
- p_to: dateRange?.to ?? null
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 = React26__default.useMemo(
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] = React26__default.useState(null);
17237
- const [loadError, setLoadError] = React26__default.useState(null);
17238
- const [expanded, setExpanded] = React26__default.useState(/* @__PURE__ */ new Set());
17239
- const [query, setQuery] = React26__default.useState("");
17240
- const [showInactive, setShowInactive] = React26__default.useState(true);
17241
- const [editing, setEditing] = React26__default.useState(null);
17242
- const [confirmDelete, setConfirmDelete] = React26__default.useState(null);
17243
- const [busy, setBusy] = React26__default.useState(false);
17244
- const initialized = React26__default.useRef(false);
17245
- const reload = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
18274
+ React29__default.useEffect(() => {
17261
18275
  void reload();
17262
18276
  }, [reload]);
17263
- const shown = React26__default.useMemo(
18277
+ const shown = React29__default.useMemo(
17264
18278
  () => (nodes ?? []).filter((n) => showInactive || n.isActive),
17265
18279
  [nodes, showInactive]
17266
18280
  );
17267
- const tree = React26__default.useMemo(() => buildChartTree(shown), [shown]);
17268
- const fullTree = React26__default.useMemo(() => buildChartTree(nodes ?? []), [nodes]);
17269
- const search = React26__default.useMemo(() => searchVisibleIds(tree, query), [tree, query]);
17270
- const groupIds = React26__default.useMemo(
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 = React26__default.useCallback((q, selfId) => {
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) => React26__default.createElement(ChartOfAccountsTree, { ...props, entity: chartOfAccountsEntity, mockData: chartOfAccountsSeed });
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 }) => React26__default.createElement(BankPicker, {
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({ config, provider, store: store6 }) {
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
- React26__default.useEffect(() => {
19655
+ React29__default.useEffect(() => {
18637
19656
  void fetchCashSessions?.();
18638
19657
  }, [fetchCashSessions]);
18639
- React26__default.useEffect(() => {
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] = React26__default.useState(rule?.name ?? "");
20661
- const [description, setDescription] = React26__default.useState(rule?.description ?? "");
20662
- const [priority, setPriority] = React26__default.useState(String(rule?.priority ?? 1));
20663
- const [isActive, setIsActive] = React26__default.useState(rule?.isActive ?? true);
20664
- const [isDefault, setIsDefault] = React26__default.useState(rule?.isDefault ?? !hasDefault);
20665
- const [saving, setSaving] = React26__default.useState(false);
20666
- const [error, setError] = React26__default.useState(null);
20667
- const initial = React26__default.useMemo(() => {
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] = React26__default.useState(initial.services);
20679
- const [categories, setCategories] = React26__default.useState(initial.categories);
20680
- const [professionals, setProfessionals] = React26__default.useState(initial.professionals);
20681
- const [units, setUnits] = React26__default.useState(initial.units);
20682
- const [weekdays, setWeekdays] = React26__default.useState(initial.weekdays);
20683
- const [ranges, setRanges] = React26__default.useState(initial.ranges);
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] = React26__default.useState(
21967
+ const [functions, setFunctions] = React29__default.useState(
20927
21968
  rule.functions.length ? rule.functions : [blankFunction(0)]
20928
21969
  );
20929
- const [saving, setSaving] = React26__default.useState(false);
20930
- const [error, setError] = React26__default.useState(null);
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] = React26__default.useState(null);
21215
- const [catalogs, setCatalogs] = React26__default.useState({
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] = React26__default.useState(null);
21222
- const [funnel, setFunnel] = React26__default.useState({});
21223
- const [editing, setEditing] = React26__default.useState(null);
21224
- const [functionsFor, setFunctionsFor] = React26__default.useState(null);
21225
- const [deleting, setDeleting] = React26__default.useState(null);
21226
- const load = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
22281
+ React29__default.useEffect(() => {
21241
22282
  void load();
21242
22283
  }, [load]);
21243
22284
  const funnelActive = Object.values(funnel).some(Boolean);
21244
- const visible = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => {
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 = React26__default.useMemo(() => [{
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 active = invoices.filter((i) => i.status !== "cancelled");
21988
- const billed = active.reduce((s, i) => s + (i.totalAmount ?? 0), 0);
21989
- const paid = active.reduce((s, i) => s + (i.paidAmount ?? 0), 0);
21990
- return { billed, paid, balance: billed - paid };
21991
- }, [invoices]);
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: fmt(totals.billed) }),
22037
- /* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryPaid"), value: fmt(totals.paid), positive: true }),
22038
- /* @__PURE__ */ jsx(Stat, { label: t2("financial.statements.columnBalance"), value: fmt(totals.balance), accent: true })
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 = React26__default.useMemo(() => counterpartyAccountingPolicy(person), [person]);
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 VIEW public.v_financial_statement_accounts
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 = React26__default.useMemo(() => createSafeFinancialProvider(), []);
46632
- const store6 = React26__default.useMemo(() => createFinancialStore(provider), [provider]);
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 = () => React26__default.createElement(PluginSettingsPanel, {
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] = React26__default.useState(null);
46708
- const [busy, setBusy] = React26__default.useState(false);
46709
- const [viewInvoiceId, setViewInvoiceId] = React26__default.useState(null);
46710
- const [comprando, setComprando] = React26__default.useState(false);
46711
- const [buscaServico, setBuscaServico] = React26__default.useState("");
46712
- const [catalogo, setCatalogo] = React26__default.useState([]);
46713
- const [escolhidos, setEscolhidos] = React26__default.useState([]);
46714
- const [comprandoBusy, setComprandoBusy] = React26__default.useState(false);
46715
- const [erroCompra, setErroCompra] = React26__default.useState(null);
46716
- const load = React26__default.useCallback(async () => {
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
- React26__default.useEffect(() => {
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
- React26__default.useEffect(() => {
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 = React26__default.useMemo(() => createSafeFinancialProvider(), []);
47003
- const store6 = React26__default.useMemo(() => createFinancialStore(provider), [provider]);
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) => React26__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
49044
+ const PersonStatementBound = (props) => React29__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
47052
49045
  PersonStatementBound.displayName = "FinancialPersonStatement";
47053
- const SupplierFinancialSettingsBound = (props) => React26__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
49046
+ const SupplierFinancialSettingsBound = (props) => React29__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
47054
49047
  SupplierFinancialSettingsBound.displayName = "FinancialSupplierSettings";
47055
- const PageComponent = () => React26__default.createElement(FinancialPage, { config, provider, store: store6, registries });
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: (() => React26__default.createElement(CashSessionHeaderControl, { config, provider, store: store6 })),
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 = () => React26__default.createElement(PluginSettingsPanel, {
49337
+ const FinancialSettingsTab = () => React29__default.createElement(PluginSettingsPanel, {
47345
49338
  title: "Financial Settings",
47346
- generalSettings: React26__default.createElement(FinancialGeneralSettings, { config }),
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 = () => React26__default.createElement(CommissionRulesView, { provider });
49364
+ const CommissionRulesTab = () => React29__default.createElement(CommissionRulesView, { provider });
47372
49365
  CommissionRulesTab.displayName = "CommissionRulesTab";
47373
49366
  return CommissionRulesTab;
47374
49367
  })(),