@fayz-ai/plugin-financial 0.21.2 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import * as React25 from 'react';
2
- import React25__default, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
1
+ import * as React26 from 'react';
2
+ import React26__default, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
3
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, Hint, PageTabs, Tooltip, TooltipTrigger, TooltipContent, Checkbox, Popover, PopoverTrigger, PopoverContent, openPrintWindow, printDocument, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, SegmentedControl, RequiredFilter, FilterMenuRow, ChoiceList, ModalFlow, Switch, WizardSteps, KpiCard, ChartWidget, CardHeader, CardTitle, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
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';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
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
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';
@@ -908,9 +908,9 @@ function startReceiptCapture(labels) {
908
908
  });
909
909
  }
910
910
  function ReceiptAgentSurface({ onOpenDraft }) {
911
- const live = React25.useRef(onOpenDraft);
911
+ const live = React26.useRef(onOpenDraft);
912
912
  live.current = onOpenDraft;
913
- const contribution = React25.useMemo(() => ({
913
+ const contribution = React26.useMemo(() => ({
914
914
  id: "financial.payables.receipt",
915
915
  pluginId: "financial",
916
916
  role: "finance",
@@ -1404,22 +1404,22 @@ function lineFromInvoice(invoice, payee) {
1404
1404
  var openAmount = (invoice) => Math.max(0, invoice.totalAmount - invoice.paidAmount);
1405
1405
  function CnabRemittanceModal({ invoices, bankAccounts, provider, onClose }) {
1406
1406
  const { currency } = useFinancialConfig();
1407
- const [step, setStep] = React25__default.useState(0);
1408
- const [bankAccountId, setBankAccountId] = React25__default.useState("");
1409
- const [draft, setDraft] = React25__default.useState(null);
1410
- const [context, setContext] = React25__default.useState(null);
1411
- const [companyName, setCompanyName] = React25__default.useState("");
1412
- const [companyDocument, setCompanyDocument] = React25__default.useState("");
1413
- const [lines, setLines] = React25__default.useState([]);
1414
- const [companyUnsaved, setCompanyUnsaved] = React25__default.useState(false);
1415
- const [loadingContext, setLoadingContext] = React25__default.useState(false);
1416
- const [busy, setBusy] = React25__default.useState(false);
1417
- const [error, setError] = React25__default.useState("");
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("");
1418
1418
  const eligibleAccounts2 = bankAccounts.filter((account2) => account2.accountType === "bank" && account2.isActive);
1419
1419
  const total = invoices.reduce((sum, invoice) => sum + openAmount(invoice), 0);
1420
1420
  const account = eligibleAccounts2.find((candidate) => candidate.id === bankAccountId);
1421
1421
  const missingBankData = draft ? missingFields(draft, companyDocument) : [];
1422
- const items = React25__default.useMemo(() => lines.map((line) => ({
1422
+ const items = React26__default.useMemo(() => lines.map((line) => ({
1423
1423
  reference: line.invoice.id,
1424
1424
  amount: openAmount(line.invoice),
1425
1425
  paymentDate: line.paymentDate,
@@ -1444,8 +1444,8 @@ function CnabRemittanceModal({ invoices, bankAccounts, provider, onClose }) {
1444
1444
  postalCode: line.payee.postalCode
1445
1445
  }
1446
1446
  })), [lines]);
1447
- const gaps = React25__default.useMemo(() => cnabPayeeGaps(items), [items]);
1448
- const batches = React25__default.useMemo(() => {
1447
+ const gaps = React26__default.useMemo(() => cnabPayeeGaps(items), [items]);
1448
+ const batches = React26__default.useMemo(() => {
1449
1449
  const grouped = /* @__PURE__ */ new Map();
1450
1450
  for (const line of lines) {
1451
1451
  const current = grouped.get(line.paymentForm) ?? { count: 0, total: 0 };
@@ -2066,7 +2066,7 @@ function FinancialCounterpartySearchSelect({
2066
2066
  onEdit,
2067
2067
  disabled
2068
2068
  }) {
2069
- const search = React25.useCallback(async (query) => {
2069
+ const search = React26.useCallback(async (query) => {
2070
2070
  const results = await lookup.search(query);
2071
2071
  return results.map((result) => ({
2072
2072
  id: result.id,
@@ -3951,9 +3951,9 @@ var SUBMIT_DRAFT = {
3951
3951
  parameters: { type: "object", properties: {} }
3952
3952
  };
3953
3953
  function PayableDraftSurface({ config, draft }) {
3954
- const live = React25.useRef(draft);
3954
+ const live = React26.useRef(draft);
3955
3955
  live.current = draft;
3956
- const lookup = React25.useMemo(
3956
+ const lookup = React26.useMemo(
3957
3957
  () => config.contactLookup ?? createArchetypeLookup({
3958
3958
  archetype: "person",
3959
3959
  kind: "supplier",
@@ -3962,7 +3962,7 @@ function PayableDraftSurface({ config, draft }) {
3962
3962
  }),
3963
3963
  [config.contactLookup]
3964
3964
  );
3965
- React25.useEffect(() => {
3965
+ React26.useEffect(() => {
3966
3966
  liveDraft = {
3967
3967
  describe: () => live.current.describe(),
3968
3968
  setSupplier: (name, id) => live.current.setSupplier(name, id),
@@ -3973,7 +3973,7 @@ function PayableDraftSurface({ config, draft }) {
3973
3973
  liveDraft = null;
3974
3974
  };
3975
3975
  }, []);
3976
- const contribution = React25.useMemo(() => ({
3976
+ const contribution = React26.useMemo(() => ({
3977
3977
  id: "financial.payables.draft",
3978
3978
  pluginId: "financial",
3979
3979
  role: "finance",
@@ -4048,7 +4048,7 @@ function PayableDraftSurface({ config, draft }) {
4048
4048
  }
4049
4049
  function createFinancialAgentMount(deps) {
4050
4050
  const FinancialAgentMount = () => {
4051
- const contribution = React25.useMemo(() => ({
4051
+ const contribution = React26.useMemo(() => ({
4052
4052
  id: "financial.payables.global",
4053
4053
  pluginId: "financial",
4054
4054
  role: "finance",
@@ -4558,10 +4558,10 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
4558
4558
  const payMovement = useFinancialStore((s) => s.payMovement);
4559
4559
  const remaining = movement.amount - movement.paidAmount;
4560
4560
  const [saving, setSaving] = useState(false);
4561
- const command = React25__default.useRef(createPendingCommand(() => createIdempotencyKey("financial:payment"), (input) => input.movementId));
4561
+ const command = React26__default.useRef(createPendingCommand(() => createIdempotencyKey("financial:payment"), (input) => input.movementId));
4562
4562
  const [pending2, setPending] = useState(false);
4563
4563
  const [visible, setVisible] = useState(false);
4564
- React25__default.useEffect(() => {
4564
+ React26__default.useEffect(() => {
4565
4565
  requestAnimationFrame(() => setVisible(true));
4566
4566
  }, []);
4567
4567
  const source = movement.metadata ?? {};
@@ -4775,8 +4775,8 @@ function InstallmentSourceFields({
4775
4775
  }
4776
4776
  var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
4777
4777
  function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel }) {
4778
- const [open, setOpen] = React25__default.useState(false);
4779
- const [q, setQ] = React25__default.useState("");
4778
+ const [open, setOpen] = React26__default.useState(false);
4779
+ const [q, setQ] = React26__default.useState("");
4780
4780
  const selected = accounts.find((a) => a.id === value);
4781
4781
  const list = q ? accounts.filter((a) => norm2(a.name).includes(norm2(q))) : accounts;
4782
4782
  const pick = (id) => {
@@ -4960,6 +4960,212 @@ function InstallmentReceiptButton({ onClick, title, attached, pending: pending2,
4960
4960
  }
4961
4961
  );
4962
4962
  }
4963
+ var DESTINATIONS = [
4964
+ { value: "devolver", label: "Devolver ao fornecedor" },
4965
+ { value: "perda", label: "Baixar como perda" },
4966
+ { value: "manter", label: "Manter no estoque" }
4967
+ ];
4968
+ function day(iso) {
4969
+ const [y, m, d] = iso.slice(0, 10).split("-");
4970
+ return `${d}/${m}/${y}`;
4971
+ }
4972
+ function CancelInvoiceDialog({ invoiceId, counterpartyName, totalAmount, items, preselectItemId, onClose, onCancelled }) {
4973
+ const { currency } = useFinancialConfig();
4974
+ const provider = useFinancialProvider();
4975
+ const [selected, setSelected] = useState(
4976
+ () => new Set(preselectItemId ? [preselectItemId] : items.map((i) => i.id))
4977
+ );
4978
+ const [reasons, setReasons] = useState(null);
4979
+ const [reasonId, setReasonId] = useState();
4980
+ const [detail, setDetail] = useState("");
4981
+ const [entries, setEntries] = useState(null);
4982
+ const [destinations, setDestinations] = useState({});
4983
+ const [busy, setBusy] = useState(false);
4984
+ const requestKey = useRef(createIdempotencyKey("financial:invoice-cancel-items"));
4985
+ useEffect(() => {
4986
+ let alive = true;
4987
+ Promise.all([
4988
+ provider.listCancellationReasons?.() ?? Promise.resolve([]),
4989
+ provider.getInvoiceStockEntries?.(invoiceId) ?? Promise.resolve([])
4990
+ ]).then(([r, e]) => {
4991
+ if (alive) {
4992
+ setReasons(r);
4993
+ setEntries(e);
4994
+ }
4995
+ }).catch((err) => {
4996
+ if (alive) {
4997
+ setReasons([]);
4998
+ setEntries([]);
4999
+ toast.error("N\xE3o foi poss\xEDvel carregar o cancelamento", { description: err?.message });
5000
+ }
5001
+ });
5002
+ return () => {
5003
+ alive = false;
5004
+ };
5005
+ }, [provider, invoiceId]);
5006
+ const whole = items.length > 0 && items.every((i) => selected.has(i.id));
5007
+ const amount = whole ? totalAmount : items.filter((i) => selected.has(i.id)).reduce((s, i) => s + i.totalAmount, 0);
5008
+ const stocked = useMemo(() => {
5009
+ const byItem = /* @__PURE__ */ new Map();
5010
+ for (const e of entries ?? []) {
5011
+ const cur = byItem.get(e.itemId);
5012
+ byItem.set(e.itemId, { entered: (cur?.entered ?? 0) + e.entered, enteredOn: cur?.enteredOn ?? e.enteredOn });
5013
+ }
5014
+ return byItem;
5015
+ }, [entries]);
5016
+ const stockedSelected = items.filter((i) => selected.has(i.id) && stocked.has(i.id));
5017
+ 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;
5018
+ function toggle(id, on) {
5019
+ setSelected((prev) => {
5020
+ const next = new Set(prev);
5021
+ if (on) next.add(id);
5022
+ else next.delete(id);
5023
+ return next;
5024
+ });
5025
+ }
5026
+ async function submit() {
5027
+ if (missing || !reasonId || !provider.cancelInvoiceItems) return;
5028
+ setBusy(true);
5029
+ try {
5030
+ const result = await provider.cancelInvoiceItems({
5031
+ invoiceId,
5032
+ itemIds: whole ? [] : [...selected],
5033
+ reasonId,
5034
+ detail,
5035
+ stock: stockedSelected.map((i) => ({
5036
+ itemId: i.id,
5037
+ destination: destinations[i.id].destination,
5038
+ note: destinations[i.id]?.note
5039
+ })),
5040
+ idempotencyKey: requestKey.current
5041
+ });
5042
+ if (result.creditAmount > 0) {
5043
+ toast.success(result.whole ? "Conta cancelada" : "Itens cancelados", {
5044
+ description: `${formatCurrency(result.creditAmount, currency)} em pagamentos ficaram como cr\xE9dito${counterpartyName ? ` de ${counterpartyName}` : ""}.`
5045
+ });
5046
+ } else {
5047
+ toast.success(result.whole ? "Conta cancelada" : "Itens cancelados");
5048
+ }
5049
+ onCancelled();
5050
+ } catch (err) {
5051
+ toast.error("N\xE3o foi poss\xEDvel cancelar", { description: err?.message });
5052
+ requestKey.current = createIdempotencyKey("financial:invoice-cancel-items");
5053
+ } finally {
5054
+ setBusy(false);
5055
+ }
5056
+ }
5057
+ const loading = reasons === null || entries === null;
5058
+ return /* @__PURE__ */ jsxs(
5059
+ ModalTask,
5060
+ {
5061
+ open: true,
5062
+ title: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 text-destructive", children: [
5063
+ /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4" }),
5064
+ " Cancelar"
5065
+ ] }),
5066
+ width: "max-w-lg",
5067
+ onClose,
5068
+ children: [
5069
+ /* @__PURE__ */ jsxs("div", { className: "fz-container min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4", children: [
5070
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
5071
+ "Selecione o que cancelar. Pagamentos j\xE1 alocados ao que for cancelado ser\xE3o liberados como cr\xE9dito",
5072
+ counterpartyName ? ` de ${counterpartyName}` : "",
5073
+ "."
5074
+ ] }),
5075
+ 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: [
5076
+ /* @__PURE__ */ jsx(
5077
+ Checkbox,
5078
+ {
5079
+ checked: whole,
5080
+ indeterminate: !whole && selected.size > 0,
5081
+ color: "destructive",
5082
+ onChange: (on) => setSelected(on ? new Set(items.map((i) => i.id)) : /* @__PURE__ */ new Set())
5083
+ }
5084
+ ),
5085
+ "Cancelar a conta inteira"
5086
+ ] }),
5087
+ /* @__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: [
5088
+ /* @__PURE__ */ jsx(Checkbox, { checked: selected.has(it.id), onChange: (on) => toggle(it.id, on) }),
5089
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm", children: it.description }),
5090
+ loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-16" }) : stocked.has(it.id) && /* @__PURE__ */ jsx(Badge, { variant: "success", className: "shrink-0", children: "No estoque" }),
5091
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(it.totalAmount, currency) })
5092
+ ] }, it.id)) }),
5093
+ stockedSelected.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-4 rounded-lg border border-warning/40 bg-warning-soft p-3", children: [
5094
+ /* @__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." }),
5095
+ stockedSelected.map((it) => {
5096
+ const info = stocked.get(it.id);
5097
+ const choice = destinations[it.id];
5098
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
5099
+ /* @__PURE__ */ jsxs("div", { children: [
5100
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: it.description }),
5101
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
5102
+ "Entrou no estoque: ",
5103
+ info.entered.toLocaleString("pt-BR"),
5104
+ " \xB7 ",
5105
+ day(info.enteredOn)
5106
+ ] })
5107
+ ] }),
5108
+ /* @__PURE__ */ jsx(
5109
+ SegmentedControl,
5110
+ {
5111
+ "aria-label": `Destino de ${it.description}`,
5112
+ options: DESTINATIONS,
5113
+ value: choice?.destination ?? "",
5114
+ onChange: (v) => setDestinations((d) => ({ ...d, [it.id]: { note: d[it.id]?.note ?? "", destination: v || void 0 } })),
5115
+ className: "flex-wrap"
5116
+ }
5117
+ ),
5118
+ /* @__PURE__ */ jsx(
5119
+ "input",
5120
+ {
5121
+ value: choice?.note ?? "",
5122
+ onChange: (e) => setDestinations((d) => ({ ...d, [it.id]: { destination: d[it.id]?.destination, note: e.target.value } })),
5123
+ placeholder: "Observa\xE7\xE3o (ex.: nota de devolu\xE7\xE3o 8226400)",
5124
+ className: "h-9 w-full rounded-input border border-input bg-card px-3 text-sm"
5125
+ }
5126
+ )
5127
+ ] }, it.id);
5128
+ })
5129
+ ] }),
5130
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t pt-3 text-sm font-medium", children: [
5131
+ /* @__PURE__ */ jsx("span", { children: whole ? "Conta inteira ser\xE1 cancelada" : "Total a cancelar" }),
5132
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatCurrency(amount, currency) })
5133
+ ] }),
5134
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
5135
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: "Motivo do cancelamento" }),
5136
+ reasons === null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }) : /* @__PURE__ */ jsx(
5137
+ ChoiceSelect,
5138
+ {
5139
+ "aria-label": "Motivo do cancelamento",
5140
+ options: reasons.map((r) => ({ value: r.id, label: r.name })),
5141
+ value: reasonId,
5142
+ onChange: setReasonId,
5143
+ placeholder: "Selecione um motivo..."
5144
+ }
5145
+ ),
5146
+ /* @__PURE__ */ jsx(
5147
+ "textarea",
5148
+ {
5149
+ value: detail,
5150
+ onChange: (e) => setDetail(e.target.value),
5151
+ placeholder: "Detalhe (opcional)...",
5152
+ className: "min-h-20 w-full resize-y rounded-input border border-input bg-card px-3 py-2 text-sm"
5153
+ }
5154
+ )
5155
+ ] })
5156
+ ] }),
5157
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3", children: [
5158
+ missing && !loading && /* @__PURE__ */ jsx("span", { className: "mr-auto text-xs text-muted-foreground", children: missing }),
5159
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: "Voltar" }),
5160
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: submit, disabled: busy || loading || !!missing, children: [
5161
+ /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
5162
+ busy ? "Cancelando\u2026" : whole ? "Cancelar conta" : "Cancelar itens"
5163
+ ] })
5164
+ ] })
5165
+ ]
5166
+ }
5167
+ );
5168
+ }
4963
5169
 
4964
5170
  // src/views/focusItem.ts
4965
5171
  var CHAVE = "fayz:financial:focus-item";
@@ -4984,9 +5190,9 @@ function consumirItemParaAbrir(invoiceId) {
4984
5190
  }
4985
5191
  }
4986
5192
  function useStockedItemTypes(itemTypes, entityLookups) {
4987
- const [empty, setEmpty] = React25__default.useState(null);
5193
+ const [empty, setEmpty] = React26__default.useState(null);
4988
5194
  const backed = itemTypes.filter((type) => entityLookups[type.value]?.count).map((type) => type.value).sort().join(",");
4989
- React25__default.useEffect(() => {
5195
+ React26__default.useEffect(() => {
4990
5196
  const kinds = backed ? backed.split(",") : [];
4991
5197
  if (!kinds.length) {
4992
5198
  setEmpty(null);
@@ -5007,7 +5213,7 @@ function useStockedItemTypes(itemTypes, entityLookups) {
5007
5213
  cancelled = true;
5008
5214
  };
5009
5215
  }, [backed]);
5010
- return React25__default.useMemo(
5216
+ return React26__default.useMemo(
5011
5217
  () => empty?.size ? itemTypes.filter((type) => !empty.has(type.value)) : itemTypes,
5012
5218
  [itemTypes, empty]
5013
5219
  );
@@ -5177,14 +5383,14 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5177
5383
  };
5178
5384
  }, [giftCardsEnabled, provider, t2]);
5179
5385
  const entityLookups = useMemo(() => giftCardLookup ? { ...config.entityLookups, gift_card: giftCardLookup } : config.entityLookups, [config.entityLookups, giftCardLookup]);
5180
- const translatedItemTypes = React25__default.useMemo(
5386
+ const translatedItemTypes = React26__default.useMemo(
5181
5387
  () => itemTypesRaw.map((tp) => {
5182
5388
  const traduzido = t2(`financial.itemType.${tp.value}`);
5183
5389
  return traduzido === `financial.itemType.${tp.value}` ? tp : { ...tp, label: traduzido };
5184
5390
  }),
5185
5391
  [itemTypesRaw, t2]
5186
5392
  );
5187
- const offeredItemTypes = React25__default.useMemo(
5393
+ const offeredItemTypes = React26__default.useMemo(
5188
5394
  () => availableInvoiceItemTypes(
5189
5395
  giftCardsEnabled && !translatedItemTypes.some((type) => type.value === "gift_card") ? [...translatedItemTypes, { value: "gift_card", label: t2("financial.itemType.gift_card"), icon: "Gift" }] : translatedItemTypes,
5190
5396
  Object.keys(entityLookups),
@@ -5208,7 +5414,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5208
5414
  const isEdit = !!editId;
5209
5415
  const [attachmentsOpen, setAttachmentsOpen] = useState(false);
5210
5416
  const [attachmentCount, setAttachmentCount] = useState(0);
5211
- const loadAttachmentCount = React25__default.useCallback(() => {
5417
+ const loadAttachmentCount = React26__default.useCallback(() => {
5212
5418
  if (!editId || !provider.getInvoiceAttachments) return;
5213
5419
  provider.getInvoiceAttachments(editId).then((list) => setAttachmentCount(list.length)).catch(() => {
5214
5420
  });
@@ -5306,16 +5512,17 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5306
5512
  } : void 0);
5307
5513
  const [saving, setSaving] = useState(false);
5308
5514
  const [menuOpen, setMenuOpen] = useState(false);
5309
- const [confirmCancel, setConfirmCancel] = useState(false);
5310
- const [cancelling, setCancelling] = useState(false);
5311
- const [cancelReason, setCancelReason] = useState("");
5312
- const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-cancel"));
5515
+ const [cancelData, setCancelData] = useState(null);
5516
+ const [cancelledLines, setCancelledLines] = useState([]);
5313
5517
  const menuRef = useRef(null);
5314
- const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
5315
- function closeCancelDialog() {
5316
- setConfirmCancel(false);
5317
- setCancelReason("");
5318
- cancelRequestKey.current = createIdempotencyKey("financial:invoice-cancel");
5518
+ async function openCancelDialog() {
5519
+ if (!editId) return;
5520
+ try {
5521
+ const [inv, lines] = await Promise.all([provider.getInvoiceById(editId), provider.getInvoiceItems(editId)]);
5522
+ setCancelData({ items: lines.filter((l) => !l.cancelledAt), total: inv?.totalAmount ?? 0, name: inv?.contactName });
5523
+ } catch (err) {
5524
+ toast.error("N\xE3o foi poss\xEDvel abrir o cancelamento", { description: err?.message });
5525
+ }
5319
5526
  }
5320
5527
  useEffect(() => {
5321
5528
  if (!menuOpen) return;
@@ -5325,21 +5532,6 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5325
5532
  document.addEventListener("mousedown", handleClick);
5326
5533
  return () => document.removeEventListener("mousedown", handleClick);
5327
5534
  }, [menuOpen]);
5328
- async function handleCancelInvoice() {
5329
- if (!editId) return;
5330
- setCancelling(true);
5331
- try {
5332
- await cancelInvoice({
5333
- invoiceId: editId,
5334
- reason: cancelReason,
5335
- idempotencyKey: cancelRequestKey.current
5336
- });
5337
- onSaved?.();
5338
- } catch {
5339
- } finally {
5340
- setCancelling(false);
5341
- }
5342
- }
5343
5535
  useEffect(() => {
5344
5536
  if (!editId || loaded) return;
5345
5537
  setLoading(true);
@@ -5366,7 +5558,9 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5366
5558
  setSavedTotal(invoice.totalAmount);
5367
5559
  setFiscalNumber(String(invoice.metadata?.fiscalNumber ?? ""));
5368
5560
  setObservations(invoice.observations ?? "");
5369
- const invoiceItems = await provider.getInvoiceItems(editId);
5561
+ const allLines = await provider.getInvoiceItems(editId);
5562
+ const invoiceItems = allLines.filter((it) => !it.cancelledAt);
5563
+ setCancelledLines(allLines.filter((it) => !!it.cancelledAt));
5370
5564
  setSavedItemsKey(itemsKey(invoiceItems));
5371
5565
  const idsCarregados = [];
5372
5566
  setItems(invoiceItems.map((it) => {
@@ -5503,13 +5697,13 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5503
5697
  if (isEdit || totalAmount <= 0 || installments.length > 0) return;
5504
5698
  generateInstallments(1);
5505
5699
  }, [isEdit, totalAmount, installments.length]);
5506
- const fromCard = React25__default.useMemo(
5700
+ const fromCard = React26__default.useMemo(
5507
5701
  () => fromCardId ? bankAccounts.find((a) => a.id === fromCardId) : void 0,
5508
5702
  [fromCardId, bankAccounts]
5509
5703
  );
5510
5704
  const cardLast4 = (fromCard?.accountNumber ?? "").replace(/\D/g, "").slice(-4);
5511
5705
  const nomeJaTemOsDigitos = !!cardLast4 && (fromCard?.name ?? "").replace(/\D/g, "").endsWith(cardLast4);
5512
- const cardMethodName = React25__default.useMemo(() => {
5706
+ const cardMethodName = React26__default.useMemo(() => {
5513
5707
  const id = installments[0]?.payMethodTypeId;
5514
5708
  const escolhida = id ? paymentMethodTypes.find((tp) => tp.id === id) : void 0;
5515
5709
  return (escolhida ?? paymentMethodTypes.find((tp) => tp.transactionType === "credit_card"))?.name;
@@ -5797,14 +5991,13 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5797
5991
  "button",
5798
5992
  {
5799
5993
  onClick: () => {
5800
- setConfirmCancel(true);
5994
+ void openCancelDialog();
5801
5995
  setMenuOpen(false);
5802
5996
  },
5803
5997
  className: "flex w-full items-center gap-2 px-3 py-2 text-xs text-destructive transition-colors hover:bg-destructive/10",
5804
5998
  children: [
5805
5999
  /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
5806
- " ",
5807
- t2("financial.invoiceForm.cancelInvoice")
6000
+ " Cancelar"
5808
6001
  ]
5809
6002
  }
5810
6003
  ) })
@@ -6104,7 +6297,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6104
6297
  sideSections,
6105
6298
  children: [
6106
6299
  contactSection,
6107
- /* @__PURE__ */ jsx(
6300
+ /* @__PURE__ */ jsxs(
6108
6301
  OrderFormItemsCard,
6109
6302
  {
6110
6303
  "data-tour": "financial.items",
@@ -6129,112 +6322,129 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6129
6322
  cancelLabel: t2("financial.invoiceForm.cancel")
6130
6323
  }
6131
6324
  ),
6132
- children: items.map((item, i) => /* @__PURE__ */ jsx(
6133
- OrderFormItemRow,
6134
- {
6135
- index: i,
6136
- itemKind: item.itemKind,
6137
- description: item.description,
6138
- quantity: item.quantity,
6139
- unitPrice: item.unitPrice,
6140
- discount: item.discount,
6141
- surcharge: item.surcharge,
6142
- referenceId: item.referenceId,
6143
- itemTypes,
6144
- currency,
6145
- formatMoney: money4,
6146
- expanded: expandedItemId === item._id,
6147
- readOnly: !canEditItems,
6148
- onToggle: () => setExpandedItemId(expandedItemId === item._id ? null : item._id),
6149
- onChange: (patch) => {
6150
- void applyItemPatch(item, patch);
6151
- },
6152
- onRemove: () => {
6153
- removeItem(item._id);
6154
- if (expandedItemId === item._id) setExpandedItemId(null);
6155
- },
6156
- extraFieldGroups: [
6157
- ...postable.length > 0 ? [{
6158
- id: "account",
6159
- label: t2("financial.invoiceForm.account"),
6160
- filled: !!item.accountId,
6161
- // O plano de contas na linha fechada: classificar deixa de
6162
- // ser trabalho invisível. Ver OrderFormExtraFieldGroup.summary.
6163
- summary: postable.find((a) => a.id === item.accountId)?.name,
6164
- onClear: () => updateItem(item._id, { accountId: void 0 }),
6165
- field: (
6166
- // Busca, não <select>: um plano de contas real tem dezenas
6167
- // ou centenas de folhas, e a lista nativa obriga a rolar
6168
- // atrás de um código que a pessoa já sabe de cor.
6169
- /* @__PURE__ */ jsx(
6325
+ children: [
6326
+ items.map((item, i) => /* @__PURE__ */ jsx(
6327
+ OrderFormItemRow,
6328
+ {
6329
+ index: i,
6330
+ itemKind: item.itemKind,
6331
+ description: item.description,
6332
+ quantity: item.quantity,
6333
+ unitPrice: item.unitPrice,
6334
+ discount: item.discount,
6335
+ surcharge: item.surcharge,
6336
+ referenceId: item.referenceId,
6337
+ itemTypes,
6338
+ currency,
6339
+ formatMoney: money4,
6340
+ expanded: expandedItemId === item._id,
6341
+ readOnly: !canEditItems,
6342
+ onToggle: () => setExpandedItemId(expandedItemId === item._id ? null : item._id),
6343
+ onChange: (patch) => {
6344
+ void applyItemPatch(item, patch);
6345
+ },
6346
+ onRemove: () => {
6347
+ removeItem(item._id);
6348
+ if (expandedItemId === item._id) setExpandedItemId(null);
6349
+ },
6350
+ extraFieldGroups: [
6351
+ ...postable.length > 0 ? [{
6352
+ id: "account",
6353
+ label: t2("financial.invoiceForm.account"),
6354
+ filled: !!item.accountId,
6355
+ // O plano de contas na linha fechada: classificar deixa de
6356
+ // ser trabalho invisível. Ver OrderFormExtraFieldGroup.summary.
6357
+ summary: postable.find((a) => a.id === item.accountId)?.name,
6358
+ onClear: () => updateItem(item._id, { accountId: void 0 }),
6359
+ field: (
6360
+ // Busca, não <select>: um plano de contas real tem dezenas
6361
+ // ou centenas de folhas, e a lista nativa obriga a rolar
6362
+ // atrás de um código que a pessoa já sabe de cor.
6363
+ /* @__PURE__ */ jsx(
6364
+ SearchSelect,
6365
+ {
6366
+ label: t2("financial.invoiceForm.account"),
6367
+ value: item.accountId,
6368
+ displayValue: postable.find((a) => a.id === item.accountId)?.name,
6369
+ placeholder: t2("financial.invoiceForm.accountNone"),
6370
+ minChars: 0,
6371
+ noResultsLabel: t2("financial.invoiceForm.accountNone"),
6372
+ onSearch: (q) => filterByCodeOrName(selectableAccounts, q).map((a) => ({
6373
+ id: a.id,
6374
+ label: accountLabel(a),
6375
+ subtitle: a.accountType ?? void 0
6376
+ })),
6377
+ onChange: (id) => updateItem(item._id, { accountId: id || void 0 }),
6378
+ onManage: canManageFinancial ? () => adminNavigateTo(CHART_OF_ACCOUNTS_ROUTE) : void 0,
6379
+ manageLabel: t2("financial.invoiceForm.manageChart"),
6380
+ clearLabel: t2("financial.invoiceForm.clear")
6381
+ }
6382
+ )
6383
+ )
6384
+ }] : [],
6385
+ ...costCenters.length > 0 ? [{
6386
+ id: "costCenter",
6387
+ label: t2("financial.invoiceForm.costCenter"),
6388
+ filled: !!item.costCenterId,
6389
+ onClear: () => updateItem(item._id, { costCenterId: void 0 }),
6390
+ field: /* @__PURE__ */ jsx(
6170
6391
  SearchSelect,
6171
6392
  {
6172
- label: t2("financial.invoiceForm.account"),
6173
- value: item.accountId,
6174
- displayValue: postable.find((a) => a.id === item.accountId)?.name,
6175
- placeholder: t2("financial.invoiceForm.accountNone"),
6393
+ label: t2("financial.invoiceForm.costCenter"),
6394
+ value: item.costCenterId,
6395
+ displayValue: costCenters.find((c) => c.id === item.costCenterId)?.name,
6396
+ placeholder: t2("financial.invoiceForm.costCenterNone"),
6176
6397
  minChars: 0,
6177
- noResultsLabel: t2("financial.invoiceForm.accountNone"),
6178
- onSearch: (q) => filterByCodeOrName(selectableAccounts, q).map((a) => ({
6179
- id: a.id,
6180
- label: accountLabel(a),
6181
- subtitle: a.accountType ?? void 0
6398
+ noResultsLabel: t2("financial.invoiceForm.costCenterNone"),
6399
+ onSearch: (q) => filterByCodeOrName(costCenters, q).map((c) => ({
6400
+ id: c.id,
6401
+ label: c.code ? `${c.code} - ${c.name}` : c.name
6182
6402
  })),
6183
- onChange: (id) => updateItem(item._id, { accountId: id || void 0 }),
6184
- onManage: canManageFinancial ? () => adminNavigateTo(CHART_OF_ACCOUNTS_ROUTE) : void 0,
6185
- manageLabel: t2("financial.invoiceForm.manageChart"),
6403
+ onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 }),
6404
+ onManage: canManageFinancial ? () => adminNavigateTo(COST_CENTERS_ROUTE) : void 0,
6405
+ manageLabel: t2("financial.invoiceForm.manageCostCenters"),
6186
6406
  clearLabel: t2("financial.invoiceForm.clear")
6187
6407
  }
6188
6408
  )
6189
- )
6190
- }] : [],
6191
- ...costCenters.length > 0 ? [{
6192
- id: "costCenter",
6193
- label: t2("financial.invoiceForm.costCenter"),
6194
- filled: !!item.costCenterId,
6195
- onClear: () => updateItem(item._id, { costCenterId: void 0 }),
6196
- field: /* @__PURE__ */ jsx(
6197
- SearchSelect,
6198
- {
6199
- label: t2("financial.invoiceForm.costCenter"),
6200
- value: item.costCenterId,
6201
- displayValue: costCenters.find((c) => c.id === item.costCenterId)?.name,
6202
- placeholder: t2("financial.invoiceForm.costCenterNone"),
6203
- minChars: 0,
6204
- noResultsLabel: t2("financial.invoiceForm.costCenterNone"),
6205
- onSearch: (q) => filterByCodeOrName(costCenters, q).map((c) => ({
6206
- id: c.id,
6207
- label: c.code ? `${c.code} - ${c.name}` : c.name
6208
- })),
6209
- onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 }),
6210
- onManage: canManageFinancial ? () => adminNavigateTo(COST_CENTERS_ROUTE) : void 0,
6211
- manageLabel: t2("financial.invoiceForm.manageCostCenters"),
6212
- clearLabel: t2("financial.invoiceForm.clear")
6213
- }
6214
- )
6215
- }] : []
6216
- ],
6217
- entityLookups,
6218
- pickOnly: Boolean(entityLookups[item.itemKind]?.expand) && !item.referenceId,
6219
- showTypeSelector: hasItemLookups,
6220
- allowSurcharge: true,
6221
- labels: {
6222
- item: t2("financial.invoiceForm.item"),
6223
- description: t2("financial.invoiceForm.description"),
6224
- descriptionPlaceholder: t2("financial.invoiceForm.itemDescription"),
6225
- noDescription: t2("financial.invoiceForm.noDescription"),
6226
- qty: t2("financial.invoiceForm.qty"),
6227
- unitPrice: t2("financial.invoiceForm.unitPrice"),
6228
- discount: t2("financial.invoiceForm.discount"),
6229
- surcharge: t2("financial.invoiceForm.surcharge"),
6230
- adjustments: t2("financial.invoiceForm.discountSurcharge"),
6231
- removeAdjustments: t2("financial.invoiceForm.removeAdjustments"),
6232
- done: t2("financial.invoiceForm.done"),
6233
- edit: t2("financial.invoiceForm.edit")
6234
- }
6235
- },
6236
- item._id
6237
- ))
6409
+ }] : []
6410
+ ],
6411
+ entityLookups,
6412
+ pickOnly: Boolean(entityLookups[item.itemKind]?.expand) && !item.referenceId,
6413
+ showTypeSelector: hasItemLookups,
6414
+ allowSurcharge: true,
6415
+ labels: {
6416
+ item: t2("financial.invoiceForm.item"),
6417
+ description: t2("financial.invoiceForm.description"),
6418
+ descriptionPlaceholder: t2("financial.invoiceForm.itemDescription"),
6419
+ noDescription: t2("financial.invoiceForm.noDescription"),
6420
+ qty: t2("financial.invoiceForm.qty"),
6421
+ unitPrice: t2("financial.invoiceForm.unitPrice"),
6422
+ discount: t2("financial.invoiceForm.discount"),
6423
+ surcharge: t2("financial.invoiceForm.surcharge"),
6424
+ adjustments: t2("financial.invoiceForm.discountSurcharge"),
6425
+ removeAdjustments: t2("financial.invoiceForm.removeAdjustments"),
6426
+ done: t2("financial.invoiceForm.done"),
6427
+ edit: t2("financial.invoiceForm.edit")
6428
+ }
6429
+ },
6430
+ item._id
6431
+ )),
6432
+ cancelledLines.map((line) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 border-t py-2 pl-4 pr-11 text-muted-foreground", children: [
6433
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
6434
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm line-through", children: line.description }),
6435
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs", children: [
6436
+ "Cancelado",
6437
+ line.cancelReason ? ` \u2014 ${line.cancelReason}` : ""
6438
+ ] })
6439
+ ] }),
6440
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs tabular-nums line-through", children: [
6441
+ line.quantity,
6442
+ " \xD7 ",
6443
+ money4(line.unitPrice)
6444
+ ] }),
6445
+ /* @__PURE__ */ jsx("span", { className: "w-24 shrink-0 text-right text-sm tabular-nums line-through", children: money4(line.totalAmount) })
6446
+ ] }, line.id))
6447
+ ]
6238
6448
  }
6239
6449
  ),
6240
6450
  scheduleSection
@@ -6277,37 +6487,19 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6277
6487
  }
6278
6488
  }
6279
6489
  ),
6280
- confirmCancel && createPortal(
6281
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick: closeCancelDialog, children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-modal border bg-card shadow-lg mx-4 p-5", onClick: (e) => e.stopPropagation(), style: { animation: "field-slide-in 200ms ease-out" }, children: [
6282
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
6283
- /* @__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" }) }),
6284
- /* @__PURE__ */ jsxs("div", { children: [
6285
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.invoiceForm.cancelInvoice") }),
6286
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoiceForm.cancelConfirm") })
6287
- ] })
6288
- ] }),
6289
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoiceForm.cancelDesc") }),
6290
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-form-cancel-reason", children: t2("financial.invoice.cancelReason") }),
6291
- /* @__PURE__ */ jsx(
6292
- "textarea",
6293
- {
6294
- id: "invoice-form-cancel-reason",
6295
- value: cancelReason,
6296
- onChange: (event) => setCancelReason(event.target.value),
6297
- placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
6298
- className: "mb-4 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
6299
- autoFocus: true
6300
- }
6301
- ),
6302
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
6303
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoiceForm.keepInvoice") }),
6304
- /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancelInvoice, disabled: cancelling || !cancelReason.trim(), children: [
6305
- /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
6306
- cancelling ? t2("financial.invoiceForm.cancelling") : t2("financial.invoiceForm.yesCancel")
6307
- ] })
6308
- ] })
6309
- ] }) }),
6310
- document.body
6490
+ cancelData && editId && /* @__PURE__ */ jsx(
6491
+ CancelInvoiceDialog,
6492
+ {
6493
+ invoiceId: editId,
6494
+ counterpartyName: cancelData.name,
6495
+ totalAmount: cancelData.total,
6496
+ items: cancelData.items,
6497
+ onClose: () => setCancelData(null),
6498
+ onCancelled: () => {
6499
+ setCancelData(null);
6500
+ onSaved?.();
6501
+ }
6502
+ }
6311
6503
  ),
6312
6504
  /* @__PURE__ */ jsx("style", { children: `
6313
6505
  @keyframes field-slide-in {
@@ -6507,17 +6699,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6507
6699
  const [attachmentsOpen, setAttachmentsOpen] = useState(false);
6508
6700
  const [pendingReceipt, setPendingReceipt] = useState();
6509
6701
  const [menuOpen, setMenuOpen] = useState(false);
6510
- const [confirmCancel, setConfirmCancel] = useState(false);
6511
- const [cancelling, setCancelling] = useState(false);
6512
- const [cancelReason, setCancelReason] = useState("");
6513
- const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-cancel"));
6702
+ const [cancelTarget, setCancelTarget] = useState(null);
6514
6703
  const menuRef = useRef(null);
6515
- const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
6516
- function closeCancelDialog() {
6517
- setConfirmCancel(false);
6518
- setCancelReason("");
6519
- cancelRequestKey.current = createIdempotencyKey("financial:invoice-cancel");
6520
- }
6521
6704
  async function patchSource(mov, next) {
6522
6705
  const metadata = { ...mov.metadata ?? {}, ...next };
6523
6706
  setMovements((prev) => prev.map((m) => m.id === mov.id ? { ...m, metadata } : m));
@@ -6621,24 +6804,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6621
6804
  useEffect(() => {
6622
6805
  void loadPaymentDetails();
6623
6806
  }, [invoiceId]);
6624
- async function handleCancel() {
6625
- setCancelling(true);
6626
- try {
6627
- await cancelInvoice({
6628
- invoiceId,
6629
- reason: cancelReason,
6630
- idempotencyKey: cancelRequestKey.current
6631
- });
6632
- await loadData();
6633
- closeCancelDialog();
6634
- setMenuOpen(false);
6635
- } catch {
6636
- } finally {
6637
- setCancelling(false);
6638
- }
6639
- }
6640
- const [chartNames, setChartNames] = React25__default.useState(/* @__PURE__ */ new Map());
6641
- React25__default.useEffect(() => {
6807
+ const [chartNames, setChartNames] = React26__default.useState(/* @__PURE__ */ new Map());
6808
+ React26__default.useEffect(() => {
6642
6809
  let cancelled2 = false;
6643
6810
  void provider.getChartOfAccounts().then((rows) => {
6644
6811
  if (!cancelled2) setChartNames(new Map(rows.map((r) => [r.id, r.name])));
@@ -6675,9 +6842,11 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6675
6842
  statusConfig.icon;
6676
6843
  const totalPaid = movements.reduce((sum, m) => sum + m.paidAmount, 0);
6677
6844
  const totalRemaining = invoice.totalAmount - totalPaid;
6678
- const subtotal = items.reduce((sum, it) => sum + it.quantity * it.unitPrice, 0);
6679
- const totalDiscount = items.reduce((sum, it) => sum + it.discount, 0);
6680
- const totalSurcharge = items.reduce((sum, it) => sum + it.surcharge, 0);
6845
+ const counted = items.filter((it) => !it.cancelledAt);
6846
+ const subtotal = counted.reduce((sum, it) => sum + it.quantity * it.unitPrice, 0);
6847
+ const totalDiscount = counted.reduce((sum, it) => sum + it.discount, 0);
6848
+ const totalSurcharge = counted.reduce((sum, it) => sum + it.surcharge, 0);
6849
+ const liveItems = items.filter((it) => !it.cancelledAt);
6681
6850
  const detailItems = items.map((it, i) => ({
6682
6851
  id: it.id ?? String(i),
6683
6852
  // Uma linha sem texto próprio herda o da fatura — melhor repetir o assunto
@@ -6686,7 +6855,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6686
6855
  // `itemKind` é vocabulário do banco, não do balcão: 'other' não diz nada a
6687
6856
  // ninguém e era o que sobrava quando a linha vinha sem nome. Só aparece
6688
6857
  // quando tem uma tradução de verdade.
6689
- description: itemKindLabel(it.itemKind, t2),
6858
+ description: it.cancelledAt ? `Cancelado${it.cancelReason ? ` \u2014 ${it.cancelReason}` : ""}` : itemKindLabel(it.itemKind, t2),
6859
+ struck: !!it.cancelledAt,
6690
6860
  // A classificação vai no campo dela, que o arquétipo desenha como pastilha
6691
6861
  // no meio da linha — o mesmo lugar que ela ocupa no formulário.
6692
6862
  account: it.accountId ? chartNames.get(it.accountId) : void 0,
@@ -6695,7 +6865,10 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6695
6865
  // a coluna de valores não dança) e leva para a edição com ESTA linha
6696
6866
  // aberta — sem ele, conferir um item e querer corrigi-lo custava abrir a
6697
6867
  // edição e reencontrar ali o que o dedo já estava apontando.
6698
- ...onEdit && invoice.status !== "cancelled" && movements.every((m) => m.paidAmount <= 0) ? { actions: [{ id: "edit", label: t2("financial.invoiceForm.edit"), icon: Pencil, onSelect: () => onEdit(i) }] } : {},
6868
+ ...invoice.status !== "cancelled" && !it.cancelledAt ? { actions: [
6869
+ ...onEdit && movements.every((m) => m.paidAmount <= 0) ? [{ id: "edit", label: t2("financial.invoiceForm.edit"), icon: Pencil, onSelect: () => onEdit(i) }] : [],
6870
+ { id: "cancel", label: "Cancelar", icon: Ban, onSelect: () => setCancelTarget(it.id) }
6871
+ ] } : {},
6699
6872
  quantity: it.quantity,
6700
6873
  unitPrice: formatCurrency(it.unitPrice, currency),
6701
6874
  total: formatCurrency(it.totalAmount, currency)
@@ -6715,9 +6888,10 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6715
6888
  /* @__PURE__ */ jsx(
6716
6889
  PageActions,
6717
6890
  {
6718
- actions: !cancelled && invoice.status !== "paid" ? [
6719
- ...onEdit ? [{ id: "edit", label: t2("financial.invoice.edit"), icon: Pencil, intent: "primary", onSelect: () => onEdit() }] : [],
6720
- { id: "cancel", label: t2("financial.invoice.cancelInvoice"), icon: Ban, intent: "danger", onSelect: () => setConfirmCancel(true) }
6891
+ actions: !cancelled ? [
6892
+ ...onEdit && invoice.status !== "paid" ? [{ id: "edit", label: t2("financial.invoice.edit"), icon: Pencil, intent: "primary", onSelect: () => onEdit() }] : [],
6893
+ // Also on a paid bill: what was paid becomes the counterparty's credit (V1).
6894
+ { id: "cancel", label: "Cancelar", icon: Ban, intent: "danger", onSelect: () => setCancelTarget("") }
6721
6895
  ] : []
6722
6896
  }
6723
6897
  )
@@ -6753,7 +6927,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6753
6927
  actions,
6754
6928
  stats: [
6755
6929
  { id: "total", label: t2("financial.invoice.total"), value: formatCurrency(invoice.totalAmount, currency), icon: DollarSign },
6756
- { id: "items", label: t2("financial.invoice.items"), value: items.length, icon: LayoutList },
6930
+ { id: "items", label: t2("financial.invoice.items"), value: liveItems.length, icon: LayoutList },
6757
6931
  { id: "installments", label: t2("financial.invoice.installments"), value: `${invoice.totalInstallments}x`, icon: Layers },
6758
6932
  {
6759
6933
  id: "date",
@@ -6990,38 +7164,20 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6990
7164
  }
6991
7165
  }
6992
7166
  ),
6993
- confirmCancel && /* @__PURE__ */ jsxs(
6994
- ModalTask,
7167
+ cancelTarget !== null && /* @__PURE__ */ jsx(
7168
+ CancelInvoiceDialog,
6995
7169
  {
6996
- open: true,
6997
- title: t2("financial.invoice.cancelInvoice"),
6998
- hint: t2("financial.invoice.cancelConfirm"),
6999
- width: "max-w-sm",
7000
- onClose: closeCancelDialog,
7001
- children: [
7002
- /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: [
7003
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoice.cancelDesc") }),
7004
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-cancel-reason", children: t2("financial.invoice.cancelReason") }),
7005
- /* @__PURE__ */ jsx(
7006
- "textarea",
7007
- {
7008
- id: "invoice-cancel-reason",
7009
- value: cancelReason,
7010
- onChange: (event) => setCancelReason(event.target.value),
7011
- placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
7012
- className: "min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
7013
- autoFocus: true
7014
- }
7015
- )
7016
- ] }),
7017
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 justify-end gap-2 border-t px-5 py-3", children: [
7018
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoice.keepInvoice") }),
7019
- /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancel, disabled: cancelling || !cancelReason.trim(), children: [
7020
- /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
7021
- cancelling ? t2("financial.invoice.cancelling") : t2("financial.invoice.yesCancel")
7022
- ] })
7023
- ] })
7024
- ]
7170
+ invoiceId: invoice.id,
7171
+ counterpartyName: invoice.contactName,
7172
+ totalAmount: invoice.totalAmount,
7173
+ items: liveItems,
7174
+ preselectItemId: cancelTarget || void 0,
7175
+ onClose: () => setCancelTarget(null),
7176
+ onCancelled: () => {
7177
+ setCancelTarget(null);
7178
+ setMenuOpen(false);
7179
+ void loadData();
7180
+ }
7025
7181
  }
7026
7182
  ),
7027
7183
  /* @__PURE__ */ jsx("style", { children: `
@@ -7635,7 +7791,7 @@ function CashRegistersView({ variant = "page" } = {}) {
7635
7791
  const [tab, setTab] = useState("open");
7636
7792
  const [reviewRows, setReviewRows] = useState(null);
7637
7793
  const [reviewError, setReviewError] = useState(null);
7638
- const loadReviews = React25__default.useCallback(async () => {
7794
+ const loadReviews = React26__default.useCallback(async () => {
7639
7795
  try {
7640
7796
  setReviewError(null);
7641
7797
  setReviewRows(await provider.getCashSessionReviews?.() ?? []);
@@ -8235,7 +8391,7 @@ function StatementsView({ onNavigate }) {
8235
8391
  "extrato"
8236
8392
  );
8237
8393
  };
8238
- const headerActions = React25__default.useMemo(() => [{
8394
+ const headerActions = React26__default.useMemo(() => [{
8239
8395
  id: "new-transfer",
8240
8396
  label: t2("financial.statements.newTransfer"),
8241
8397
  icon: ArrowRightLeft,
@@ -8431,15 +8587,15 @@ function CommissionsView() {
8431
8587
  const fetchCommissions = useFinancialStore((s) => s.fetchCommissions);
8432
8588
  const generateCommissions = useFinancialStore((s) => s.generateCommissions);
8433
8589
  const provider = useFinancialProvider();
8434
- const [from, setFrom] = React25__default.useState(firstOfMonth());
8435
- const [to, setTo] = React25__default.useState(today2());
8436
- const [preset, setPreset] = React25__default.useState("this_month");
8437
- const [running, setRunning] = React25__default.useState(false);
8438
- const [lastRun, setLastRun] = React25__default.useState(null);
8439
- const [generation, setGeneration] = React25__default.useState(null);
8440
- const [busy, setBusy] = React25__default.useState(null);
8441
- const [error, setError] = React25__default.useState(null);
8442
- const loadGeneration = React25__default.useCallback(async () => {
8590
+ const [from, setFrom] = React26__default.useState(firstOfMonth());
8591
+ const [to, setTo] = React26__default.useState(today2());
8592
+ const [preset, setPreset] = React26__default.useState("this_month");
8593
+ const [running, setRunning] = React26__default.useState(false);
8594
+ const [lastRun, setLastRun] = React26__default.useState(null);
8595
+ const [generation, setGeneration] = React26__default.useState(null);
8596
+ const [busy, setBusy] = React26__default.useState(null);
8597
+ const [error, setError] = React26__default.useState(null);
8598
+ const loadGeneration = React26__default.useCallback(async () => {
8443
8599
  try {
8444
8600
  const gens = await provider.getCommissionGenerations?.() ?? [];
8445
8601
  const doPeriodo = gens.filter((g) => g.periodStart === from && g.periodEnd === to);
@@ -8447,11 +8603,11 @@ function CommissionsView() {
8447
8603
  } catch {
8448
8604
  }
8449
8605
  }, [provider, from, to]);
8450
- React25__default.useEffect(() => {
8606
+ React26__default.useEffect(() => {
8451
8607
  void fetchCommissions({ periodStart: from, periodEnd: to });
8452
8608
  void loadGeneration();
8453
8609
  }, [fetchCommissions, loadGeneration, from, to]);
8454
- const byPerson = React25__default.useMemo(() => {
8610
+ const byPerson = React26__default.useMemo(() => {
8455
8611
  const map = /* @__PURE__ */ new Map();
8456
8612
  for (const c of commissions2) {
8457
8613
  const key = c.personId;
@@ -8504,7 +8660,7 @@ function CommissionsView() {
8504
8660
  setBusy(null);
8505
8661
  }
8506
8662
  }
8507
- const headerActions = React25__default.useMemo(() => [{
8663
+ const headerActions = React26__default.useMemo(() => [{
8508
8664
  id: "commission-rules",
8509
8665
  label: "Configurar regras de comiss\xE3o",
8510
8666
  icon: Settings,
@@ -8952,13 +9108,13 @@ function money3(v) {
8952
9108
  function GoalsView() {
8953
9109
  const { labels } = useFinancialConfig();
8954
9110
  const provider = useFinancialProvider();
8955
- const [units, setUnits] = React25__default.useState([]);
8956
- const [unitId, setUnitId] = React25__default.useState("");
8957
- const [year, setYear] = React25__default.useState((/* @__PURE__ */ new Date()).getFullYear());
8958
- const [rows, setRows] = React25__default.useState(null);
8959
- const [editing, setEditing] = React25__default.useState(null);
8960
- const [error, setError] = React25__default.useState(null);
8961
- React25__default.useEffect(() => {
9111
+ const [units, setUnits] = React26__default.useState([]);
9112
+ const [unitId, setUnitId] = React26__default.useState("");
9113
+ const [year, setYear] = React26__default.useState((/* @__PURE__ */ new Date()).getFullYear());
9114
+ const [rows, setRows] = React26__default.useState(null);
9115
+ const [editing, setEditing] = React26__default.useState(null);
9116
+ const [error, setError] = React26__default.useState(null);
9117
+ React26__default.useEffect(() => {
8962
9118
  void (async () => {
8963
9119
  const cat = await provider.getCommissionCatalogs?.();
8964
9120
  const us = cat?.units ?? [];
@@ -8966,7 +9122,7 @@ function GoalsView() {
8966
9122
  setUnitId((cur) => cur || us[0]?.id || "");
8967
9123
  })();
8968
9124
  }, [provider]);
8969
- const load = React25__default.useCallback(async () => {
9125
+ const load = React26__default.useCallback(async () => {
8970
9126
  if (!unitId) return;
8971
9127
  setRows(null);
8972
9128
  try {
@@ -8977,7 +9133,7 @@ function GoalsView() {
8977
9133
  setRows([]);
8978
9134
  }
8979
9135
  }, [provider, unitId, year]);
8980
- React25__default.useEffect(() => {
9136
+ React26__default.useEffect(() => {
8981
9137
  void load();
8982
9138
  }, [load]);
8983
9139
  const totalRealizado = (rows ?? []).reduce((s, r) => s + r.achievedAmount, 0);
@@ -9108,11 +9264,11 @@ function GoalDialog({
9108
9264
  onSaved
9109
9265
  }) {
9110
9266
  const provider = useFinancialProvider();
9111
- const [target, setTarget] = React25__default.useState(String(goal.targetAmount || ""));
9112
- const [sup, setSup] = React25__default.useState(String(goal.superAmount || ""));
9113
- const [hyper, setHyper] = React25__default.useState(String(goal.hyperAmount || ""));
9114
- const [saving, setSaving] = React25__default.useState(false);
9115
- const [error, setError] = React25__default.useState(null);
9267
+ const [target, setTarget] = React26__default.useState(String(goal.targetAmount || ""));
9268
+ const [sup, setSup] = React26__default.useState(String(goal.superAmount || ""));
9269
+ const [hyper, setHyper] = React26__default.useState(String(goal.hyperAmount || ""));
9270
+ const [saving, setSaving] = React26__default.useState(false);
9271
+ const [error, setError] = React26__default.useState(null);
9116
9272
  const t2 = Number(target) || 0;
9117
9273
  const s = Number(sup) || 0;
9118
9274
  const h = Number(hyper) || 0;
@@ -9160,7 +9316,7 @@ function GoalDialog({
9160
9316
  ladderErr && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: ladderErr }),
9161
9317
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
9162
9318
  ] }) }),
9163
- /* @__PURE__ */ jsxs(ModalFooter, { className: "border-t px-5 py-3", children: [
9319
+ /* @__PURE__ */ jsxs(ModalFooter, { variant: "bar", children: [
9164
9320
  /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: "Cancelar" }),
9165
9321
  /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => void save(), disabled: saving || !!ladderErr, children: [
9166
9322
  saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : null,
@@ -9203,21 +9359,21 @@ function GiftCardsView() {
9203
9359
  const provider = useFinancialProvider();
9204
9360
  const { currency } = useFinancialConfig();
9205
9361
  const can = usePermissionOptional();
9206
- const [enabled, setEnabled] = React25__default.useState(null);
9207
- const [cards, setCards] = React25__default.useState(null);
9208
- const [error, setError] = React25__default.useState(null);
9209
- const [issuing, setIssuing] = React25__default.useState(false);
9210
- const [issued, setIssued] = React25__default.useState(null);
9211
- const [activating, setActivating] = React25__default.useState(null);
9212
- const [search, setSearch] = React25__default.useState("");
9213
- const [state, setState] = React25__default.useState("");
9214
- const [minValue, setMinValue] = React25__default.useState("");
9215
- const [maxValue, setMaxValue] = React25__default.useState("");
9216
- const [from, setFrom] = React25__default.useState("");
9217
- const [to, setTo] = React25__default.useState("");
9218
- const money4 = React25__default.useCallback((v) => formatCurrency(v, currency), [currency]);
9219
- const seq = React25__default.useRef(0);
9220
- const load = React25__default.useCallback(async () => {
9362
+ const [enabled, setEnabled] = React26__default.useState(null);
9363
+ const [cards, setCards] = React26__default.useState(null);
9364
+ const [error, setError] = React26__default.useState(null);
9365
+ const [issuing, setIssuing] = React26__default.useState(false);
9366
+ const [issued, setIssued] = React26__default.useState(null);
9367
+ const [activating, setActivating] = React26__default.useState(null);
9368
+ const [search, setSearch] = React26__default.useState("");
9369
+ const [state, setState] = React26__default.useState("");
9370
+ const [minValue, setMinValue] = React26__default.useState("");
9371
+ const [maxValue, setMaxValue] = React26__default.useState("");
9372
+ const [from, setFrom] = React26__default.useState("");
9373
+ const [to, setTo] = React26__default.useState("");
9374
+ const money4 = React26__default.useCallback((v) => formatCurrency(v, currency), [currency]);
9375
+ const seq = React26__default.useRef(0);
9376
+ const load = React26__default.useCallback(async () => {
9221
9377
  const mine = ++seq.current;
9222
9378
  setCards(null);
9223
9379
  setError(null);
@@ -9238,7 +9394,7 @@ function GiftCardsView() {
9238
9394
  setCards([]);
9239
9395
  }
9240
9396
  }, [provider, search, state, minValue, maxValue, from, to]);
9241
- React25__default.useEffect(() => {
9397
+ React26__default.useEffect(() => {
9242
9398
  void (async () => {
9243
9399
  try {
9244
9400
  setEnabled(await provider.giftCardsEnabled?.() === true);
@@ -9247,10 +9403,10 @@ function GiftCardsView() {
9247
9403
  }
9248
9404
  })();
9249
9405
  }, [provider]);
9250
- React25__default.useEffect(() => {
9406
+ React26__default.useEffect(() => {
9251
9407
  if (enabled) void load();
9252
9408
  }, [enabled, load]);
9253
- const columns = React25__default.useMemo(() => [
9409
+ const columns = React26__default.useMemo(() => [
9254
9410
  {
9255
9411
  accessorKey: "code",
9256
9412
  header: "C\xF3digo",
@@ -9431,12 +9587,12 @@ function IssueDialog({
9431
9587
  onIssued
9432
9588
  }) {
9433
9589
  const provider = useFinancialProvider();
9434
- const [quantity, setQuantity] = React25__default.useState("10");
9435
- const [amount, setAmount] = React25__default.useState("100");
9436
- const [expiresAt, setExpiresAt] = React25__default.useState("");
9437
- const [note, setNote] = React25__default.useState("");
9438
- const [saving, setSaving] = React25__default.useState(false);
9439
- const [error, setError] = React25__default.useState(null);
9590
+ const [quantity, setQuantity] = React26__default.useState("10");
9591
+ const [amount, setAmount] = React26__default.useState("100");
9592
+ const [expiresAt, setExpiresAt] = React26__default.useState("");
9593
+ const [note, setNote] = React26__default.useState("");
9594
+ const [saving, setSaving] = React26__default.useState(false);
9595
+ const [error, setError] = React26__default.useState(null);
9440
9596
  const q = Math.trunc(Number(quantity)) || 0;
9441
9597
  const a = Number(amount) || 0;
9442
9598
  const total = q * a;
@@ -9534,8 +9690,8 @@ function IssueDialog({
9534
9690
  ] }) });
9535
9691
  }
9536
9692
  var shortDate = (value) => {
9537
- const [year, month, day] = value.split("-");
9538
- return year && month && day ? `${day}/${month}/${year}` : value;
9693
+ const [year, month, day2] = value.split("-");
9694
+ return year && month && day2 ? `${day2}/${month}/${year}` : value;
9539
9695
  };
9540
9696
  function LinhaDoExtrato({ line, fmt }) {
9541
9697
  const isCredit = line.direction === "credit";
@@ -9737,12 +9893,30 @@ async function runReconcileLink(provider, line, target, options = {}) {
9737
9893
  await provider.reconcileMovement({ bankMovementId: line.id, matchedMovementId: target.movementId });
9738
9894
  return;
9739
9895
  }
9896
+ const adjustments = (options.adjustments ?? []).filter((adj) => round22(adj.amount) > 0);
9897
+ if (provider.addInstallmentCharge && adjustments.length) {
9898
+ let due = target.openAmount;
9899
+ for (const adj of adjustments) {
9900
+ await provider.addInstallmentCharge({
9901
+ installmentId: target.movementId,
9902
+ kind: adj.type,
9903
+ amount: round22(adj.amount),
9904
+ accountId: adj.accountId,
9905
+ description: adjustmentDescription(adj)
9906
+ });
9907
+ due += adj.type === "discount" ? -round22(adj.amount) : round22(adj.amount);
9908
+ }
9909
+ const quitada2 = await provider.payMovement(buildPayment(line, target.movementId, round22(due), options.payment));
9910
+ const paymentId2 = await paymentIdOf(provider, quitada2);
9911
+ await provider.reconcileMovement({ bankMovementId: line.id, matchedMovementId: paymentId2 });
9912
+ return;
9913
+ }
9740
9914
  const quitada = await provider.payMovement(
9741
9915
  buildPayment(line, target.movementId, target.openAmount, options.payment)
9742
9916
  );
9743
9917
  const paymentId = await paymentIdOf(provider, quitada);
9744
9918
  await provider.reconcileMovement({ bankMovementId: line.id, matchedMovementId: paymentId });
9745
- for (const adj of options.adjustments ?? []) {
9919
+ for (const adj of adjustments) {
9746
9920
  if (round22(adj.amount) <= 0) continue;
9747
9921
  const direction = adjustmentDirection(line.direction, adj.type);
9748
9922
  const description = adjustmentDescription(adj);
@@ -9842,8 +10016,8 @@ function createAgentReceiptReader(deps) {
9842
10016
  };
9843
10017
  }
9844
10018
  var shortDate2 = (value) => {
9845
- const [year, month, day] = value.split("-");
9846
- return year && month && day ? `${day}/${month}/${year}` : value;
10019
+ const [year, month, day2] = value.split("-");
10020
+ return year && month && day2 ? `${day2}/${month}/${year}` : value;
9847
10021
  };
9848
10022
  var shiftDate = (value, days) => {
9849
10023
  const d = /* @__PURE__ */ new Date(`${value}T00:00:00`);
@@ -10804,8 +10978,8 @@ function ReconcileFlow({ line, initialIntent, initialTarget, initialStep = 0, on
10804
10978
  }
10805
10979
  var PAGE_SIZE = 50;
10806
10980
  var shortDate3 = (value) => {
10807
- const [year, month, day] = value.split("-");
10808
- return year && month && day ? `${day}/${month}/${year}` : value;
10981
+ const [year, month, day2] = value.split("-");
10982
+ return year && month && day2 ? `${day2}/${month}/${year}` : value;
10809
10983
  };
10810
10984
  var isoDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
10811
10985
  var ACCOUNT_GROUPS2 = {
@@ -10974,11 +11148,11 @@ function ReconciliationView() {
10974
11148
  setBusy(false);
10975
11149
  }
10976
11150
  }
10977
- const RefreshIcon = React25__default.useMemo(
11151
+ const RefreshIcon = React26__default.useMemo(
10978
11152
  () => ({ className }) => /* @__PURE__ */ jsx(RefreshCw, { className: `${className ?? ""} ${loading ? "animate-spin" : ""}` }),
10979
11153
  [loading]
10980
11154
  );
10981
- const headerActions = React25__default.useMemo(() => [{
11155
+ const headerActions = React26__default.useMemo(() => [{
10982
11156
  id: "refresh",
10983
11157
  label: t2("financial.reconciliation.refresh"),
10984
11158
  icon: RefreshIcon,
@@ -11384,8 +11558,8 @@ function today3() {
11384
11558
  }
11385
11559
  function dateLabel(value) {
11386
11560
  if (!value) return "Sem t\xE9rmino";
11387
- const [year, month, day] = value.split("-");
11388
- return year && month && day ? `${day}/${month}/${year}` : value;
11561
+ const [year, month, day2] = value.split("-");
11562
+ return year && month && day2 ? `${day2}/${month}/${year}` : value;
11389
11563
  }
11390
11564
  function newRecurringItem() {
11391
11565
  return { _id: crypto.randomUUID(), itemKind: "other", description: "", quantity: 1, unitPrice: 0 };
@@ -11412,13 +11586,13 @@ function RecurringBillsList({ direction, onNavigate }) {
11412
11586
  const provider = useFinancialProvider();
11413
11587
  const { currency } = useFinancialConfig();
11414
11588
  const can = usePermissionOptional();
11415
- const [rows, setRows] = React25__default.useState(null);
11416
- const [search, setSearch] = React25__default.useState("");
11417
- const [status, setStatus] = React25__default.useState("all");
11418
- const [changing, setChanging] = React25__default.useState(null);
11589
+ const [rows, setRows] = React26__default.useState(null);
11590
+ const [search, setSearch] = React26__default.useState("");
11591
+ const [status, setStatus] = React26__default.useState("all");
11592
+ const [changing, setChanging] = React26__default.useState(null);
11419
11593
  const isPayable = direction === "payable";
11420
11594
  const prefix = isPayable ? "payables" : "receivables";
11421
- const load = React25__default.useCallback(async () => {
11595
+ const load = React26__default.useCallback(async () => {
11422
11596
  setRows(null);
11423
11597
  try {
11424
11598
  setRows(await provider.getRecurringBills?.(direction) ?? []);
@@ -11427,10 +11601,10 @@ function RecurringBillsList({ direction, onNavigate }) {
11427
11601
  toast.error(error?.message ?? "N\xE3o foi poss\xEDvel carregar as recorr\xEAncias.");
11428
11602
  }
11429
11603
  }, [provider, direction]);
11430
- React25__default.useEffect(() => {
11604
+ React26__default.useEffect(() => {
11431
11605
  void load();
11432
11606
  }, [load]);
11433
- const filtered = React25__default.useMemo(() => {
11607
+ const filtered = React26__default.useMemo(() => {
11434
11608
  const term = search.trim().toLowerCase();
11435
11609
  const visiveis = (rows ?? []).filter((row) => {
11436
11610
  if (status === "active" && !row.isActive) return false;
@@ -11440,7 +11614,7 @@ function RecurringBillsList({ direction, onNavigate }) {
11440
11614
  });
11441
11615
  return [...visiveis].sort((a, b) => Number(b.isActive) - Number(a.isActive) || (a.dueDay ?? 0) - (b.dueDay ?? 0) || a.description.localeCompare(b.description));
11442
11616
  }, [rows, search, status]);
11443
- const porMes = React25__default.useMemo(() => {
11617
+ const porMes = React26__default.useMemo(() => {
11444
11618
  const fator = { weekly: 52 / 12, monthly: 1, quarterly: 1 / 3, yearly: 1 / 12 };
11445
11619
  const ativas = filtered.filter((row) => row.isActive);
11446
11620
  return {
@@ -11461,7 +11635,7 @@ function RecurringBillsList({ direction, onNavigate }) {
11461
11635
  setChanging(null);
11462
11636
  }
11463
11637
  }
11464
- const columns = React25__default.useMemo(() => [
11638
+ const columns = React26__default.useMemo(() => [
11465
11639
  {
11466
11640
  accessorKey: "description",
11467
11641
  header: "Descri\xE7\xE3o",
@@ -11559,41 +11733,41 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11559
11733
  const { locations, currency, itemTypes: configuredItemTypes, entityLookups } = useFinancialConfig();
11560
11734
  const t2 = useTranslation();
11561
11735
  const isPayable = direction === "payable";
11562
- const [value, setValue] = React25__default.useState(null);
11563
- const [loaded, setLoaded] = React25__default.useState(!valueId);
11564
- const [contact, setContact] = React25__default.useState(null);
11565
- const [counterpartyAccounting, setCounterpartyAccounting] = React25__default.useState({
11736
+ const [value, setValue] = React26__default.useState(null);
11737
+ const [loaded, setLoaded] = React26__default.useState(!valueId);
11738
+ const [contact, setContact] = React26__default.useState(null);
11739
+ const [counterpartyAccounting, setCounterpartyAccounting] = React26__default.useState({
11566
11740
  allowedAccountIds: [],
11567
11741
  limitAccounts: false,
11568
11742
  addItemsAutomatically: false
11569
11743
  });
11570
- const accountingPolicyRequest = React25__default.useRef(0);
11571
- const counterpartyLookup = React25__default.useMemo(
11744
+ const accountingPolicyRequest = React26__default.useRef(0);
11745
+ const counterpartyLookup = React26__default.useMemo(
11572
11746
  () => createFinancialCounterpartyLookup(isPayable ? "debit" : "credit"),
11573
11747
  [isPayable]
11574
11748
  );
11575
11749
  const contactCreate = useLookupCreate(counterpartyLookup, (created) => {
11576
11750
  setContact({ id: created.id, name: created.label });
11577
11751
  });
11578
- const initialItem = React25__default.useRef(newRecurringItem());
11579
- const [items, setItems] = React25__default.useState([initialItem.current]);
11580
- const [expandedItemId, setExpandedItemId] = React25__default.useState(initialItem.current._id);
11581
- const [addingItem, setAddingItem] = React25__default.useState(false);
11582
- const [frequency, setFrequency] = React25__default.useState("monthly");
11583
- const [dueDay, setDueDay] = React25__default.useState("1");
11584
- const [startsOn, setStartsOn] = React25__default.useState(today3());
11585
- const [endsOn, setEndsOn] = React25__default.useState("");
11586
- const [unitId, setUnitId] = React25__default.useState(locations[0]?.id ?? "");
11587
- const [availableLocations, setAvailableLocations] = React25__default.useState(locations);
11588
- const [notes, setNotes] = React25__default.useState("");
11589
- const [isActive, setIsActive] = React25__default.useState(true);
11590
- const [accounts, setAccounts] = React25__default.useState([]);
11591
- const [costCenters, setCostCenters] = React25__default.useState([]);
11752
+ const initialItem = React26__default.useRef(newRecurringItem());
11753
+ const [items, setItems] = React26__default.useState([initialItem.current]);
11754
+ const [expandedItemId, setExpandedItemId] = React26__default.useState(initialItem.current._id);
11755
+ const [addingItem, setAddingItem] = React26__default.useState(false);
11756
+ const [frequency, setFrequency] = React26__default.useState("monthly");
11757
+ const [dueDay, setDueDay] = React26__default.useState("1");
11758
+ const [startsOn, setStartsOn] = React26__default.useState(today3());
11759
+ const [endsOn, setEndsOn] = React26__default.useState("");
11760
+ const [unitId, setUnitId] = React26__default.useState(locations[0]?.id ?? "");
11761
+ const [availableLocations, setAvailableLocations] = React26__default.useState(locations);
11762
+ const [notes, setNotes] = React26__default.useState("");
11763
+ const [isActive, setIsActive] = React26__default.useState(true);
11764
+ const [accounts, setAccounts] = React26__default.useState([]);
11765
+ const [costCenters, setCostCenters] = React26__default.useState([]);
11592
11766
  const { choices: unitChoices } = useWritableUnits(availableLocations);
11593
- const [saving, setSaving] = React25__default.useState(false);
11594
- const [ending, setEnding] = React25__default.useState(false);
11595
- const [error, setError] = React25__default.useState(null);
11596
- React25__default.useEffect(() => {
11767
+ const [saving, setSaving] = React26__default.useState(false);
11768
+ const [ending, setEnding] = React26__default.useState(false);
11769
+ const [error, setError] = React26__default.useState(null);
11770
+ React26__default.useEffect(() => {
11597
11771
  if (locations.length > 0) {
11598
11772
  setAvailableLocations(locations);
11599
11773
  return;
@@ -11608,7 +11782,7 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11608
11782
  cancelled = true;
11609
11783
  };
11610
11784
  }, [locations]);
11611
- React25__default.useEffect(() => {
11785
+ React26__default.useEffect(() => {
11612
11786
  let cancelled = false;
11613
11787
  void Promise.all([
11614
11788
  provider.getChartOfAccounts(),
@@ -11655,25 +11829,25 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11655
11829
  cancelled = true;
11656
11830
  };
11657
11831
  }, [provider, direction, valueId]);
11658
- const translatedItemTypes = React25__default.useMemo(() => configuredItemTypes.map((tp) => {
11832
+ const translatedItemTypes = React26__default.useMemo(() => configuredItemTypes.map((tp) => {
11659
11833
  const label = t2(`financial.itemType.${tp.value}`);
11660
11834
  return label === `financial.itemType.${tp.value}` ? tp : { ...tp, label };
11661
11835
  }), [configuredItemTypes, t2]);
11662
- const offeredItemTypes = React25__default.useMemo(() => availableInvoiceItemTypes(
11836
+ const offeredItemTypes = React26__default.useMemo(() => availableInvoiceItemTypes(
11663
11837
  translatedItemTypes,
11664
11838
  Object.keys(entityLookups),
11665
11839
  t2("financial.itemType.other"),
11666
11840
  direction
11667
11841
  ), [translatedItemTypes, entityLookups, direction, t2]);
11668
11842
  const itemTypes = useStockedItemTypes(offeredItemTypes, entityLookups);
11669
- const selectableAccounts = React25__default.useMemo(() => {
11843
+ const selectableAccounts = React26__default.useMemo(() => {
11670
11844
  const postable = postableAccounts(accounts);
11671
11845
  if (!counterpartyAccounting.limitAccounts) return postable;
11672
11846
  const allowed = new Set(counterpartyAccounting.allowedAccountIds);
11673
11847
  return postable.filter((account) => allowed.has(account.id));
11674
11848
  }, [accounts, counterpartyAccounting]);
11675
11849
  const hasItemLookups = itemTypes.some((item) => entityLookups[item.value]);
11676
- const amount = React25__default.useMemo(() => items.reduce(
11850
+ const amount = React26__default.useMemo(() => items.reduce(
11677
11851
  (sum, item) => sum + item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
11678
11852
  0
11679
11853
  ), [items]);
@@ -11681,8 +11855,8 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11681
11855
  const numericDueDay = Math.trunc(Number(dueDay));
11682
11856
  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;
11683
11857
  const fields = { items, contactId: contact?.id ?? "", frequency, dueDay, startsOn, endsOn, unitId, notes, isActive };
11684
- const snapshot = React25__default.useRef(null);
11685
- React25__default.useEffect(() => {
11858
+ const snapshot = React26__default.useRef(null);
11859
+ React26__default.useEffect(() => {
11686
11860
  if (loaded && snapshot.current === null) snapshot.current = JSON.stringify(fields);
11687
11861
  }, [loaded]);
11688
11862
  const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
@@ -12529,7 +12703,7 @@ function BalanceHero() {
12529
12703
  const summary = useFinancialStore((s) => s.summary);
12530
12704
  const navigate = useDashboardNavigate();
12531
12705
  useEnsureSummary();
12532
- const [hidden, setHidden] = React25__default.useState(false);
12706
+ const [hidden, setHidden] = React26__default.useState(false);
12533
12707
  const money4 = (v) => hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022" : formatCurrency(v ?? 0, currency);
12534
12708
  const inflow = summary?.monthlyInflow ?? 0;
12535
12709
  const outflow = summary?.monthlyOutflow ?? 0;
@@ -12602,7 +12776,7 @@ function categorizeSpend(notes) {
12602
12776
  function SpendByCategoryChart() {
12603
12777
  const t2 = useTranslation();
12604
12778
  const provider = useFinancialProvider();
12605
- const [rows, setRows] = React25__default.useState([]);
12779
+ const [rows, setRows] = React26__default.useState([]);
12606
12780
  useEffect(() => {
12607
12781
  let alive = true;
12608
12782
  void (async () => {
@@ -12645,7 +12819,7 @@ function UpcomingBills() {
12645
12819
  const { currency } = useFinancialConfig();
12646
12820
  const provider = useFinancialProvider();
12647
12821
  const dueLabel = useDueLabel();
12648
- const [bills, setBills] = React25__default.useState([]);
12822
+ const [bills, setBills] = React26__default.useState([]);
12649
12823
  useEffect(() => {
12650
12824
  let alive = true;
12651
12825
  void (async () => {
@@ -14248,7 +14422,8 @@ function createSupabaseFinancialProvider() {
14248
14422
  createInvoice: (input) => provider.createInvoice(input),
14249
14423
  getMovements: (query) => provider.getMovements(query),
14250
14424
  payMovement: (input) => provider.payMovement(input),
14251
- reconcileMovement: ({ bankMovementId, matchedMovementId }) => markReconciled(bankMovementId, matchedMovementId)
14425
+ reconcileMovement: ({ bankMovementId, matchedMovementId }) => markReconciled(bankMovementId, matchedMovementId),
14426
+ addInstallmentCharge: (input) => provider.addInstallmentCharge(input)
14252
14427
  };
14253
14428
  }
14254
14429
  const provider = {
@@ -14397,7 +14572,9 @@ function createSupabaseFinancialProvider() {
14397
14572
  costCenterId: i.costCenterId,
14398
14573
  // De onde a linha veio — o pacote que a gerou, por exemplo. Sem
14399
14574
  // devolvê-lo, editar a fatura desfazia o vínculo em silêncio.
14400
- metadata: i.metadata ?? void 0,
14575
+ metadata: i.metadata ?? i.itemMetadata ?? void 0,
14576
+ cancelledAt: i.itemCancelledAt ?? void 0,
14577
+ cancelReason: i.itemCancelReason ?? void 0,
14401
14578
  createdAt: i.createdAt
14402
14579
  };
14403
14580
  });
@@ -14585,7 +14762,7 @@ function createSupabaseFinancialProvider() {
14585
14762
  const totalAmount = round26(items.reduce((sum, i) => sum + lineTotal(i), 0));
14586
14763
  const { error: totalErr } = await pub.from("plg_financial_invoices").update({ total: totalAmount, description: items.map((i) => i.description).filter(Boolean).join(", ") || null }).eq("id", invoiceId);
14587
14764
  if (totalErr) throw totalErr;
14588
- const { error: delErr } = await core.from("items").delete().eq("invoice_id", invoiceId);
14765
+ const { error: delErr } = await core.from("items").delete().eq("invoice_id", invoiceId).is("cancelled_at", null);
14589
14766
  if (delErr) throw delErr;
14590
14767
  if (items.length === 0) return;
14591
14768
  const { data: inserted, error: insErr } = await core.from("items").insert(
@@ -14639,6 +14816,57 @@ function createSupabaseFinancialProvider() {
14639
14816
  });
14640
14817
  if (error) throw error;
14641
14818
  },
14819
+ async listCancellationReasons() {
14820
+ const { pub } = getClients();
14821
+ const { data, error } = await pub.rpc("financial_cancellation_reasons");
14822
+ if (error) throw error;
14823
+ return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
14824
+ },
14825
+ async getInvoiceStockEntries(invoiceId) {
14826
+ const { pub } = getClients();
14827
+ const { data, error } = await pub.rpc("financial_invoice_stock_entries", { p_invoice_id: invoiceId });
14828
+ if (error) throw error;
14829
+ return (data ?? []).map((r) => ({
14830
+ itemId: r.item_id,
14831
+ productId: r.product_id,
14832
+ locationId: r.location_id,
14833
+ entered: Number(r.entered ?? 0),
14834
+ available: Number(r.available ?? 0),
14835
+ enteredOn: r.entered_on
14836
+ }));
14837
+ },
14838
+ async cancelInvoiceItems(input) {
14839
+ const { pub } = getClients();
14840
+ const { data, error } = await pub.rpc("financial_cancel_invoice_items", {
14841
+ p_invoice_id: input.invoiceId,
14842
+ p_item_ids: input.itemIds.length ? input.itemIds : null,
14843
+ p_reason_id: input.reasonId,
14844
+ p_detail: input.detail?.trim() || null,
14845
+ p_stock: input.stock.map((s) => ({ item_id: s.itemId, destination: s.destination, note: s.note?.trim() || null })),
14846
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:invoice-cancel-items")
14847
+ });
14848
+ if (error) throw error;
14849
+ const r = data ?? {};
14850
+ return { whole: !!r.whole, amount: Number(r.amount ?? 0), creditAmount: Number(r.credit_amount ?? 0) };
14851
+ },
14852
+ async addInstallmentCharge(input) {
14853
+ const { pub } = getClients();
14854
+ const { error } = await pub.rpc("financial_add_installment_charge", {
14855
+ p_installment_id: input.installmentId,
14856
+ p_kind: input.kind,
14857
+ p_amount: input.amount,
14858
+ p_chart_account_id: input.accountId ?? null,
14859
+ p_description: input.description?.trim() || null,
14860
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:installment-charge")
14861
+ });
14862
+ if (error) throw error;
14863
+ },
14864
+ async getSupplierCredit(personId) {
14865
+ const { pub } = getClients();
14866
+ const { data, error } = await pub.rpc("financial_supplier_credit", { p_person_id: personId });
14867
+ if (error) throw error;
14868
+ return Number(data ?? 0);
14869
+ },
14642
14870
  // --- Movements (public.financial_movements) ---
14643
14871
  async getMovements(query) {
14644
14872
  const { pub } = getClients();
@@ -16996,21 +17224,21 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
16996
17224
  const canCreate = can("financial", "create");
16997
17225
  const canEdit = can("financial", "edit");
16998
17226
  const canDelete = can("financial", "delete");
16999
- const provider = React25__default.useMemo(
17227
+ const provider = React26__default.useMemo(
17000
17228
  () => dataProvider ?? resolveDataProvider(entity, mockData ?? []),
17001
17229
  [entity, mockData, dataProvider]
17002
17230
  );
17003
17231
  const table = entity.data?.table;
17004
- const [nodes, setNodes] = React25__default.useState(null);
17005
- const [loadError, setLoadError] = React25__default.useState(null);
17006
- const [expanded, setExpanded] = React25__default.useState(/* @__PURE__ */ new Set());
17007
- const [query, setQuery] = React25__default.useState("");
17008
- const [showInactive, setShowInactive] = React25__default.useState(true);
17009
- const [editing, setEditing] = React25__default.useState(null);
17010
- const [confirmDelete, setConfirmDelete] = React25__default.useState(null);
17011
- const [busy, setBusy] = React25__default.useState(false);
17012
- const initialized = React25__default.useRef(false);
17013
- const reload = React25__default.useCallback(async () => {
17232
+ const [nodes, setNodes] = React26__default.useState(null);
17233
+ const [loadError, setLoadError] = React26__default.useState(null);
17234
+ const [expanded, setExpanded] = React26__default.useState(/* @__PURE__ */ new Set());
17235
+ const [query, setQuery] = React26__default.useState("");
17236
+ const [showInactive, setShowInactive] = React26__default.useState(true);
17237
+ const [editing, setEditing] = React26__default.useState(null);
17238
+ const [confirmDelete, setConfirmDelete] = React26__default.useState(null);
17239
+ const [busy, setBusy] = React26__default.useState(false);
17240
+ const initialized = React26__default.useRef(false);
17241
+ const reload = React26__default.useCallback(async () => {
17014
17242
  try {
17015
17243
  const rows = await loadAll(provider);
17016
17244
  setNodes(rows);
@@ -17025,17 +17253,17 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
17025
17253
  setNodes((prev) => prev ?? []);
17026
17254
  }
17027
17255
  }, [provider]);
17028
- React25__default.useEffect(() => {
17256
+ React26__default.useEffect(() => {
17029
17257
  void reload();
17030
17258
  }, [reload]);
17031
- const shown = React25__default.useMemo(
17259
+ const shown = React26__default.useMemo(
17032
17260
  () => (nodes ?? []).filter((n) => showInactive || n.isActive),
17033
17261
  [nodes, showInactive]
17034
17262
  );
17035
- const tree = React25__default.useMemo(() => buildChartTree(shown), [shown]);
17036
- const fullTree = React25__default.useMemo(() => buildChartTree(nodes ?? []), [nodes]);
17037
- const search = React25__default.useMemo(() => searchVisibleIds(tree, query), [tree, query]);
17038
- const groupIds = React25__default.useMemo(
17263
+ const tree = React26__default.useMemo(() => buildChartTree(shown), [shown]);
17264
+ const fullTree = React26__default.useMemo(() => buildChartTree(nodes ?? []), [nodes]);
17265
+ const search = React26__default.useMemo(() => searchVisibleIds(tree, query), [tree, query]);
17266
+ const groupIds = React26__default.useMemo(
17039
17267
  () => [...tree.byId.values()].filter((n) => isGroupNode(tree, n)).map((n) => n.id),
17040
17268
  [tree]
17041
17269
  );
@@ -17131,7 +17359,7 @@ function ChartOfAccountsTree({ entity, mockData, dataProvider, embedded }) {
17131
17359
  const ok = await run(() => provider.remove(node.id), t2("financial.chartTree.deleted", { name: node.name }));
17132
17360
  if (ok) setConfirmDelete(null);
17133
17361
  }
17134
- const parentOptions = React25__default.useCallback((q, selfId) => {
17362
+ const parentOptions = React26__default.useCallback((q, selfId) => {
17135
17363
  const blocked = selfId ? descendantIds(fullTree, selfId).add(selfId) : /* @__PURE__ */ new Set();
17136
17364
  const needle = q.trim().toLowerCase();
17137
17365
  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) }));
@@ -17434,7 +17662,7 @@ var chartOfAccountsSeed = [
17434
17662
  { id: "coa-4.2", code: "4.2", name: "Utilities", nodeType: "leaf", accountType: "expense", parentId: "coa-4", isActive: true },
17435
17663
  { id: "coa-4.3", code: "4.3", name: "Payroll", nodeType: "leaf", accountType: "expense", parentId: "coa-4", isActive: true }
17436
17664
  ];
17437
- var ChartOfAccountsRegistryView = (props) => React25__default.createElement(ChartOfAccountsTree, { ...props, entity: chartOfAccountsEntity, mockData: chartOfAccountsSeed });
17665
+ var ChartOfAccountsRegistryView = (props) => React26__default.createElement(ChartOfAccountsTree, { ...props, entity: chartOfAccountsEntity, mockData: chartOfAccountsSeed });
17438
17666
  ChartOfAccountsRegistryView.displayName = "ChartOfAccountsRegistryView";
17439
17667
  var costCenterEntity = {
17440
17668
  name: "Cost Center",
@@ -17532,7 +17760,7 @@ var bankAccountEntity = {
17532
17760
  labelWhen: (v) => v.kind === "card_payable" ? "Banco emissor" : void 0,
17533
17761
  visibleWhen: (v) => hasIssuer(v.kind),
17534
17762
  group: "bank",
17535
- renderInput: ({ value, values, onChange, setValue }) => React25__default.createElement(BankPicker, {
17763
+ renderInput: ({ value, values, onChange, setValue }) => React26__default.createElement(BankPicker, {
17536
17764
  code: values.bankCode ?? "",
17537
17765
  name: value ?? "",
17538
17766
  onChange: (next) => {
@@ -18401,10 +18629,10 @@ function CashSessionHeaderControl({ config, provider, store: store6 }) {
18401
18629
  const fetchCashSessions = useStore(store6, (s) => s.fetchCashSessions);
18402
18630
  const open = useStore(store6, (s) => s.cashSessionModalOpen);
18403
18631
  const setOpen = useStore(store6, (s) => s.setCashSessionModalOpen);
18404
- React25__default.useEffect(() => {
18632
+ React26__default.useEffect(() => {
18405
18633
  void fetchCashSessions?.();
18406
18634
  }, [fetchCashSessions]);
18407
- React25__default.useEffect(() => {
18635
+ React26__default.useEffect(() => {
18408
18636
  if (!open) return;
18409
18637
  const close = () => setOpen(false);
18410
18638
  window.addEventListener("hashchange", close);
@@ -20425,14 +20653,14 @@ function CommissionRuleDialog({
20425
20653
  onClose,
20426
20654
  onSaved
20427
20655
  }) {
20428
- const [name, setName] = React25__default.useState(rule?.name ?? "");
20429
- const [description, setDescription] = React25__default.useState(rule?.description ?? "");
20430
- const [priority, setPriority] = React25__default.useState(String(rule?.priority ?? 1));
20431
- const [isActive, setIsActive] = React25__default.useState(rule?.isActive ?? true);
20432
- const [isDefault, setIsDefault] = React25__default.useState(rule?.isDefault ?? !hasDefault);
20433
- const [saving, setSaving] = React25__default.useState(false);
20434
- const [error, setError] = React25__default.useState(null);
20435
- const initial = React25__default.useMemo(() => {
20656
+ const [name, setName] = React26__default.useState(rule?.name ?? "");
20657
+ const [description, setDescription] = React26__default.useState(rule?.description ?? "");
20658
+ const [priority, setPriority] = React26__default.useState(String(rule?.priority ?? 1));
20659
+ const [isActive, setIsActive] = React26__default.useState(rule?.isActive ?? true);
20660
+ const [isDefault, setIsDefault] = React26__default.useState(rule?.isDefault ?? !hasDefault);
20661
+ const [saving, setSaving] = React26__default.useState(false);
20662
+ const [error, setError] = React26__default.useState(null);
20663
+ const initial = React26__default.useMemo(() => {
20436
20664
  const get = (t2, key = "ids") => rule?.criteria.find((c) => c.criteriaType === t2)?.criteriaValue?.[key] ?? [];
20437
20665
  return {
20438
20666
  services: get("service"),
@@ -20443,12 +20671,12 @@ function CommissionRuleDialog({
20443
20671
  ranges: get("time_range", "ranges")
20444
20672
  };
20445
20673
  }, [rule]);
20446
- const [services, setServices] = React25__default.useState(initial.services);
20447
- const [categories, setCategories] = React25__default.useState(initial.categories);
20448
- const [professionals, setProfessionals] = React25__default.useState(initial.professionals);
20449
- const [units, setUnits] = React25__default.useState(initial.units);
20450
- const [weekdays, setWeekdays] = React25__default.useState(initial.weekdays);
20451
- const [ranges, setRanges] = React25__default.useState(initial.ranges);
20674
+ const [services, setServices] = React26__default.useState(initial.services);
20675
+ const [categories, setCategories] = React26__default.useState(initial.categories);
20676
+ const [professionals, setProfessionals] = React26__default.useState(initial.professionals);
20677
+ const [units, setUnits] = React26__default.useState(initial.units);
20678
+ const [weekdays, setWeekdays] = React26__default.useState(initial.weekdays);
20679
+ const [ranges, setRanges] = React26__default.useState(initial.ranges);
20452
20680
  const criteriaCount = [services, categories, professionals, units].filter((a) => a.length > 0).length + (weekdays.length ? 1 : 0) + (ranges.length ? 1 : 0);
20453
20681
  async function handleSave() {
20454
20682
  if (!name.trim()) {
@@ -20642,7 +20870,7 @@ function CommissionRuleDialog({
20642
20870
  ] }),
20643
20871
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
20644
20872
  ] }) }),
20645
- /* @__PURE__ */ jsxs(ModalFooter, { className: "border-t px-5 py-3", children: [
20873
+ /* @__PURE__ */ jsxs(ModalFooter, { variant: "bar", children: [
20646
20874
  /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: "Cancelar" }),
20647
20875
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: handleSave, disabled: saving || !name.trim(), children: saving ? "Salvando\u2026" : "Salvar" })
20648
20876
  ] })
@@ -20691,11 +20919,11 @@ function blankFunction(order) {
20691
20919
  };
20692
20920
  }
20693
20921
  function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
20694
- const [functions, setFunctions] = React25__default.useState(
20922
+ const [functions, setFunctions] = React26__default.useState(
20695
20923
  rule.functions.length ? rule.functions : [blankFunction(0)]
20696
20924
  );
20697
- const [saving, setSaving] = React25__default.useState(false);
20698
- const [error, setError] = React25__default.useState(null);
20925
+ const [saving, setSaving] = React26__default.useState(false);
20926
+ const [error, setError] = React26__default.useState(null);
20699
20927
  function patch(i, changes) {
20700
20928
  setFunctions((fs) => fs.map((f, j) => j === i ? { ...f, ...changes } : f));
20701
20929
  }
@@ -20823,7 +21051,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
20823
21051
  ] }),
20824
21052
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
20825
21053
  ] }) }),
20826
- /* @__PURE__ */ jsxs(ModalFooter, { className: "border-t px-5 py-3", children: [
21054
+ /* @__PURE__ */ jsxs(ModalFooter, { variant: "bar", children: [
20827
21055
  /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: "Fechar" }),
20828
21056
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: handleSave, disabled: saving, children: saving ? "Salvando\u2026" : "Salvar" })
20829
21057
  ] })
@@ -20979,19 +21207,19 @@ function functionChip(fn) {
20979
21207
  return fn.valueType === "percentage" ? `${who}: ${fn.valueAmount}%` : `${who}: R$ ${fn.valueAmount.toFixed(2)}`;
20980
21208
  }
20981
21209
  function CommissionRulesView({ provider }) {
20982
- const [rules, setRules] = React25__default.useState(null);
20983
- const [catalogs, setCatalogs] = React25__default.useState({
21210
+ const [rules, setRules] = React26__default.useState(null);
21211
+ const [catalogs, setCatalogs] = React26__default.useState({
20984
21212
  services: [],
20985
21213
  categories: [],
20986
21214
  professionals: [],
20987
21215
  units: []
20988
21216
  });
20989
- const [error, setError] = React25__default.useState(null);
20990
- const [funnel, setFunnel] = React25__default.useState({});
20991
- const [editing, setEditing] = React25__default.useState(null);
20992
- const [functionsFor, setFunctionsFor] = React25__default.useState(null);
20993
- const [deleting, setDeleting] = React25__default.useState(null);
20994
- const load = React25__default.useCallback(async () => {
21217
+ const [error, setError] = React26__default.useState(null);
21218
+ const [funnel, setFunnel] = React26__default.useState({});
21219
+ const [editing, setEditing] = React26__default.useState(null);
21220
+ const [functionsFor, setFunctionsFor] = React26__default.useState(null);
21221
+ const [deleting, setDeleting] = React26__default.useState(null);
21222
+ const load = React26__default.useCallback(async () => {
20995
21223
  try {
20996
21224
  setError(null);
20997
21225
  const [r, c] = await Promise.all([
@@ -21005,17 +21233,17 @@ function CommissionRulesView({ provider }) {
21005
21233
  setRules([]);
21006
21234
  }
21007
21235
  }, [provider]);
21008
- React25__default.useEffect(() => {
21236
+ React26__default.useEffect(() => {
21009
21237
  void load();
21010
21238
  }, [load]);
21011
21239
  const funnelActive = Object.values(funnel).some(Boolean);
21012
- const visible = React25__default.useMemo(() => {
21240
+ const visible = React26__default.useMemo(() => {
21013
21241
  if (!rules) return [];
21014
21242
  const ranked = rankRules(rules);
21015
21243
  if (!funnelActive) return ranked;
21016
21244
  return ranked.filter((r) => r.isDefault || ruleMatches(r, funnel, catalogs));
21017
21245
  }, [rules, funnel, funnelActive, catalogs]);
21018
- const winner = React25__default.useMemo(() => {
21246
+ const winner = React26__default.useMemo(() => {
21019
21247
  if (!funnelActive) return null;
21020
21248
  return visible.find((r) => r.isActive && !r.isDefault) ?? visible.find((r) => r.isActive && r.isDefault) ?? null;
21021
21249
  }, [visible, funnelActive]);
@@ -21030,7 +21258,7 @@ function CommissionRulesView({ provider }) {
21030
21258
  await provider.duplicateCommissionRule?.(rule.id);
21031
21259
  await load();
21032
21260
  }
21033
- const headerActions = React25__default.useMemo(() => [{
21261
+ const headerActions = React26__default.useMemo(() => [{
21034
21262
  id: "new-rule",
21035
21263
  label: "Adicionar regra",
21036
21264
  icon: Plus,
@@ -21812,7 +22040,7 @@ function PersonStatementWidget({ person, provider, config }) {
21812
22040
  ] });
21813
22041
  }
21814
22042
  function SupplierFinancialSettings({ person, provider }) {
21815
- const initial = React25__default.useMemo(() => counterpartyAccountingPolicy(person), [person]);
22043
+ const initial = React26__default.useMemo(() => counterpartyAccountingPolicy(person), [person]);
21816
22044
  const [accounts, setAccounts] = useState([]);
21817
22045
  const [costCenters, setCostCenters] = useState([]);
21818
22046
  const [defaultAccountId, setDefaultAccountId] = useState(initial.defaultAccountId ?? "");
@@ -32908,6 +33136,20 @@ var MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO = `-- A linha
32908
33136
  -- exige, e manter a assinatura \xE9 o que deixa esta corre\xE7\xE3o ser um replace e n\xE3o
32909
33137
  -- um drop com depend\xEAncia quebrada.
32910
33138
 
33139
+ -- Replay guard. financial/120 appended item_cancelled_at, item_cancel_reason and
33140
+ -- item_metadata to this view; re-running this narrower list over that shape
33141
+ -- fails with \xABcannot drop columns from view\xBB. Once the view has them it is left
33142
+ -- as 120 made it. The invoker setting and the grant below are idempotent.
33143
+ DO $guard$
33144
+ BEGIN
33145
+ IF EXISTS (SELECT 1 FROM pg_attribute
33146
+ WHERE attrelid = to_regclass('public.v_financial_invoice_lines')
33147
+ AND attname = 'item_cancelled_at' AND NOT attisdropped) THEN
33148
+ RAISE NOTICE 'financial 038: v_financial_invoice_lines already has the 120 shape; left as it is';
33149
+ RETURN;
33150
+ END IF;
33151
+
33152
+ EXECUTE $view$
32911
33153
  CREATE OR REPLACE VIEW public.v_financial_invoice_lines AS
32912
33154
  -- \u2500\u2500 caminho 1: a linha nasceu no pr\xF3prio t\xEDtulo \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
32913
33155
  SELECT i.id AS item_id,
@@ -32976,13 +33218,17 @@ SELECT i.id AS item_id,
32976
33218
  -- PART of an order (financial_invoice_from_lines) has its own lines, and
32977
33219
  -- without this guard it also listed every line of the order \u2014 the whole
32978
33220
  -- sale reported as billed, and summed twice.
32979
- AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id);
33221
+ AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id)
33222
+ $view$;
32980
33223
 
33224
+ EXECUTE $comment$
32981
33225
  COMMENT ON VIEW public.v_financial_invoice_lines IS
32982
33226
  'Linha de fatura com classifica\xE7\xE3o cont\xE1bil, pelos DOIS caminhos: a linha '
32983
33227
  'criada no t\xEDtulo (items.invoice_id) e a criada na venda (items.order_id = '
32984
33228
  'plg_financial_invoices.order_id). O segundo \xE9 100% do que o porte do V1 traz '
32985
- '\u2014 items_one_parent pro\xEDbe um item de ter os dois pais.';
33229
+ '\u2014 items_one_parent pro\xEDbe um item de ter os dois pais.'
33230
+ $comment$;
33231
+ END $guard$;
32986
33232
 
32987
33233
  -- \`security_invoker\` precisa sobreviver ao replace: sem ele a view passaria a
32988
33234
  -- rodar com os direitos do dono e furaria a RLS de todo mundo que a l\xEA.
@@ -45327,6 +45573,924 @@ END $function$;
45327
45573
  COMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS
45328
45574
  'Rewrites the schedule by UPDATING each numbered installment in place, and retires an unpaid legacy bill mirror instead of refusing (119).';
45329
45575
  `;
45576
+ var MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO = `-- Cancelar uma conta \xE9 escolher O QUE se cancela, POR QU\xCA e o que acontece
45577
+ -- com a mercadoria \u2014 e o que j\xE1 foi pago vira cr\xE9dito, n\xE3o some.
45578
+ --
45579
+ -- Contrato: V1, \`financeiro/contas-a-pagar-cancelamento/FLOW.md\` (09-10-2026).
45580
+ -- At\xE9 aqui o V2 cancelava s\xF3 a conta inteira, com motivo em texto livre, e
45581
+ -- RECUSAVA conta com pagamento. O V1 faz quatro coisas que este arquivo traz:
45582
+ --
45583
+ -- 1. cancela por ITEM \u2014 corta a parte n\xE3o paga das parcelas, da \xFAltima para
45584
+ -- tr\xE1s; s\xF3 depois libera o pago, do pagamento mais recente para tr\xE1s;
45585
+ -- 2. o motivo \xE9 uma LISTA da casa (Outros, Solicitado pelo cliente,
45586
+ -- Solicitado pelo profissional) + um detalhe;
45587
+ -- 3. o que j\xE1 foi pago vira CR\xC9DITO da contraparte (fornecedor ou cliente);
45588
+ -- 4. mercadoria que entrou no estoque por aquela nota pede destino:
45589
+ -- devolver ao fornecedor / baixar como perda / manter.
45590
+ --
45591
+ -- E um defeito que estava esperando por isto: pagar uma conta a pagar com
45592
+ -- \xABcr\xE9dito\xBB conferia o saldo com o sinal do CLIENTE. Cr\xE9dito de fornecedor \xE9 o
45593
+ -- fornecedor nos devendo \u2014 d\xE9bito na conta da pessoa \u2014 e s\xF3 conta o que veio
45594
+ -- de um cancelamento (ver \xA78).
45595
+ --
45596
+ -- Idempotente.
45597
+
45598
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45599
+ -- 1. Os motivos
45600
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45601
+
45602
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cancellation_reasons (
45603
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
45604
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
45605
+ name text NOT NULL CHECK (btrim(name) <> ''),
45606
+ is_active boolean NOT NULL DEFAULT true,
45607
+ created_at timestamptz NOT NULL DEFAULT now(),
45608
+ updated_at timestamptz NOT NULL DEFAULT now()
45609
+ );
45610
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellation_reasons_name_uq
45611
+ ON public.plg_financial_cancellation_reasons (tenant_id, lower(btrim(name)));
45612
+
45613
+ DO $$
45614
+ BEGIN
45615
+ CREATE TRIGGER plg_financial_cancellation_reasons_updated_at
45616
+ BEFORE UPDATE ON public.plg_financial_cancellation_reasons
45617
+ FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
45618
+ EXCEPTION WHEN duplicate_object THEN NULL;
45619
+ END $$;
45620
+
45621
+ SELECT app.scaffold_table('public.plg_financial_cancellation_reasons'::regclass,
45622
+ 'financial.invoice', 'financial', false, 'enforce');
45623
+ SELECT app.scaffold_scope('public.plg_financial_cancellation_reasons'::regclass, 'tenant', true);
45624
+
45625
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45626
+ -- 2. O registro de cada cancelamento
45627
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45628
+
45629
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cancellations (
45630
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
45631
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
45632
+ unit_id uuid,
45633
+ invoice_id uuid NOT NULL REFERENCES public.plg_financial_invoices(id) ON DELETE CASCADE,
45634
+ whole boolean NOT NULL,
45635
+ item_ids uuid[] NOT NULL DEFAULT '{}',
45636
+ reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL,
45637
+ reason text NOT NULL,
45638
+ amount numeric(14,2) NOT NULL,
45639
+ credit_amount numeric(14,2) NOT NULL DEFAULT 0,
45640
+ credit_movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,
45641
+ -- [{item_id, product_id, destination, quantity, note, operation_ids}]
45642
+ stock jsonb NOT NULL DEFAULT '[]'::jsonb,
45643
+ idempotency_key text,
45644
+ created_by uuid,
45645
+ created_at timestamptz NOT NULL DEFAULT now(),
45646
+ updated_at timestamptz NOT NULL DEFAULT now()
45647
+ );
45648
+ CREATE INDEX IF NOT EXISTS plg_financial_cancellations_invoice_idx
45649
+ ON public.plg_financial_cancellations (invoice_id);
45650
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellations_key_uq
45651
+ ON public.plg_financial_cancellations (tenant_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
45652
+
45653
+ DO $$
45654
+ BEGIN
45655
+ ALTER TABLE public.plg_financial_cancellations
45656
+ ADD CONSTRAINT plg_financial_cancellations_unit_fkey
45657
+ FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
45658
+ EXCEPTION WHEN duplicate_object THEN NULL;
45659
+ END $$;
45660
+
45661
+ DO $$
45662
+ BEGIN
45663
+ CREATE TRIGGER plg_financial_cancellations_updated_at
45664
+ BEFORE UPDATE ON public.plg_financial_cancellations
45665
+ FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
45666
+ EXCEPTION WHEN duplicate_object THEN NULL;
45667
+ END $$;
45668
+
45669
+ SELECT app.scaffold_table('public.plg_financial_cancellations'::regclass,
45670
+ 'financial.invoice', 'financial', false, 'enforce');
45671
+ SELECT app.scaffold_scope('public.plg_financial_cancellations'::regclass, 'unit_optional', true);
45672
+
45673
+ ALTER TABLE public.plg_financial_invoices
45674
+ ADD COLUMN IF NOT EXISTS cancel_reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL;
45675
+
45676
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45677
+ -- 3. A linha diz que foi cancelada
45678
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45679
+ -- Colunas novas s\xF3 no FIM: \`CREATE OR REPLACE VIEW\` n\xE3o aceita outra ordem.
45680
+
45681
+ CREATE OR REPLACE VIEW public.v_financial_invoice_lines AS
45682
+ SELECT i.id AS item_id, i.tenant_id, i.unit_id, i.invoice_id, f.number AS invoice_number,
45683
+ f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,
45684
+ i.quantity, i.unit_price, i.discount, i.total,
45685
+ COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,
45686
+ COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,
45687
+ x.tax_amount, x.cfop, x.ncm,
45688
+ a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,
45689
+ i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata
45690
+ FROM public.items i
45691
+ JOIN public.plg_financial_invoices f ON f.id = i.invoice_id
45692
+ LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id
45693
+ LEFT JOIN public.item_associations a ON a.to_item_id = i.id
45694
+ WHERE i.invoice_id IS NOT NULL
45695
+ UNION ALL
45696
+ SELECT i.id AS item_id, i.tenant_id, i.unit_id, f.id AS invoice_id, f.number AS invoice_number,
45697
+ f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,
45698
+ i.quantity, i.unit_price, i.discount, i.total,
45699
+ COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,
45700
+ COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,
45701
+ x.tax_amount, x.cfop, x.ncm,
45702
+ a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,
45703
+ i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata
45704
+ FROM public.items i
45705
+ JOIN public.plg_financial_invoices f ON f.order_id = i.order_id
45706
+ LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id
45707
+ LEFT JOIN public.item_associations a ON a.to_item_id = i.id
45708
+ WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL
45709
+ AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id);
45710
+
45711
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45712
+ -- 4. A licen\xE7a de emendar uma conta paga
45713
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45714
+ -- Conta com pagamento \xE9 imut\xE1vel (099/110) \u2014 certo para quem edita a conta,
45715
+ -- errado para quem a CANCELA ou lan\xE7a o juros que o banco cobrou. As duas RPCs
45716
+ -- deste arquivo ligam \`financial.amend\` na pr\xF3pria transa\xE7\xE3o; nenhum outro
45717
+ -- caminho liga.
45718
+
45719
+ CREATE OR REPLACE FUNCTION public.trg_invoice_line_immutable() RETURNS trigger
45720
+ LANGUAGE plpgsql SECURITY DEFINER
45721
+ SET search_path TO ''
45722
+ AS $$
45723
+ DECLARE v_issued date; v_cancelled timestamptz; v_invoice uuid; v_paid numeric;
45724
+ BEGIN
45725
+ IF coalesce(current_setting('financial.amend', true), '') = 'on' THEN RETURN coalesce(NEW, OLD); END IF;
45726
+ v_invoice := coalesce(NEW.invoice_id, OLD.invoice_id);
45727
+ IF v_invoice IS NULL THEN RETURN coalesce(NEW, OLD); END IF;
45728
+ SELECT f.issue_date, f.cancelled_at INTO v_issued, v_cancelled
45729
+ FROM public.plg_financial_invoices f WHERE f.id = v_invoice;
45730
+ IF v_issued IS NOT NULL AND v_cancelled IS NULL THEN
45731
+ SELECT coalesce(sum(b.paid), 0) INTO v_paid
45732
+ FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice;
45733
+ IF v_paid > 0 THEN
45734
+ RAISE EXCEPTION 'items: the lines of invoice % cannot be altered \u2014 % already received', v_invoice, v_paid
45735
+ USING ERRCODE = '23514',
45736
+ HINT = 'A paid invoice is immutable (PRD 04). Reverse the payments, or raise a new invoice.';
45737
+ END IF;
45738
+ END IF;
45739
+ RETURN coalesce(NEW, OLD);
45740
+ END $$;
45741
+
45742
+ CREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable()
45743
+ RETURNS trigger
45744
+ LANGUAGE plpgsql
45745
+ SECURITY DEFINER
45746
+ SET search_path TO ''
45747
+ AS $function$
45748
+ DECLARE v_paid numeric;
45749
+ BEGIN
45750
+ IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;
45751
+ IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;
45752
+
45753
+ IF NEW.direction IS DISTINCT FROM OLD.direction
45754
+ OR NEW.order_id IS DISTINCT FROM OLD.order_id
45755
+ OR NEW.number IS DISTINCT FROM OLD.number
45756
+ OR NEW.issue_date IS DISTINCT FROM OLD.issue_date
45757
+ OR NEW.currency IS DISTINCT FROM OLD.currency THEN
45758
+ RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number
45759
+ USING ERRCODE = '23514',
45760
+ HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';
45761
+ END IF;
45762
+
45763
+ -- S\xF3 o TOTAL ganha a licen\xE7a: cancelar itens e lan\xE7ar juros mudam quanto a
45764
+ -- conta vale; compet\xEAncia e contraparte continuam travadas.
45765
+ IF NEW.competence_date IS DISTINCT FROM OLD.competence_date
45766
+ OR (NEW.total IS DISTINCT FROM OLD.total AND coalesce(current_setting('financial.amend', true), '') <> 'on')
45767
+ OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id THEN
45768
+ SELECT coalesce(sum(b.paid), 0) INTO v_paid
45769
+ FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;
45770
+ IF v_paid > 0 THEN
45771
+ RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and cannot be rewritten', OLD.number, v_paid
45772
+ USING ERRCODE = '23514',
45773
+ HINT = 'Reverse the payments first, or correct it with a new invoice.';
45774
+ END IF;
45775
+ END IF;
45776
+
45777
+ RETURN NEW;
45778
+ END $function$;
45779
+
45780
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45781
+ -- 5. Os motivos da casa, semeados na primeira pergunta
45782
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45783
+
45784
+ CREATE OR REPLACE FUNCTION public.financial_cancellation_reasons()
45785
+ RETURNS SETOF public.plg_financial_cancellation_reasons
45786
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
45787
+ DECLARE v_tenant uuid := public.plg_financial_require_tenant();
45788
+ BEGIN
45789
+ PERFORM public.plg_financial_require_permission('financial.read');
45790
+ IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN
45791
+ INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)
45792
+ SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo profissional']) n
45793
+ ON CONFLICT DO NOTHING;
45794
+ END IF;
45795
+ RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r
45796
+ WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;
45797
+ END $$;
45798
+ REVOKE ALL ON FUNCTION public.financial_cancellation_reasons() FROM PUBLIC;
45799
+ GRANT EXECUTE ON FUNCTION public.financial_cancellation_reasons() TO authenticated, service_role;
45800
+
45801
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45802
+ -- 6. De onde veio a mercadoria de cada linha
45803
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45804
+ -- A nota \xE9 a ponte: a entrada no estoque diz \xABEntrada via DANFE NF-e <n\xBA>\xBB
45805
+ -- (fiscal-br/015, e as linhas migradas do V1 j\xE1 diziam). O n\xFAmero sai da nota
45806
+ -- recebida pelo V2 ou, na conta que veio do V1, do pedido de compra. O produto
45807
+ -- casa pelo id e, se o V1 tiver duplicado o cadastro, pelo nome.
45808
+ --
45809
+ -- Leitura din\xE2mica: o financeiro n\xE3o depende fisicamente do estoque; sem o
45810
+ -- plugin de estoque, nenhuma linha est\xE1 \xABno estoque\xBB.
45811
+
45812
+ CREATE OR REPLACE FUNCTION public.financial_invoice_stock_entries(p_invoice_id uuid)
45813
+ RETURNS TABLE (item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)
45814
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$
45815
+ DECLARE
45816
+ v_tenant uuid := public.plg_financial_require_tenant();
45817
+ v_inv public.plg_financial_invoices%ROWTYPE;
45818
+ v_nf text;
45819
+ BEGIN
45820
+ SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;
45821
+ IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;
45822
+ PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);
45823
+ IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;
45824
+
45825
+ IF to_regclass('public.plg_fiscalbr_inbox_documents') IS NOT NULL THEN
45826
+ EXECUTE 'SELECT d.nfe_number FROM public.plg_fiscalbr_inbox_documents d WHERE d.tenant_id = $1 AND d.invoice_id = $2 LIMIT 1'
45827
+ INTO v_nf USING v_tenant, p_invoice_id;
45828
+ END IF;
45829
+ IF v_nf IS NULL AND v_inv.order_id IS NOT NULL THEN
45830
+ SELECT nullif(btrim(o.reference_number), '') INTO v_nf FROM public.orders o WHERE o.id = v_inv.order_id;
45831
+ END IF;
45832
+ v_nf := coalesce(v_nf, nullif(btrim(v_inv.metadata ->> 'fiscalNumber'), ''));
45833
+ IF v_nf IS NULL THEN RETURN; END IF;
45834
+
45835
+ RETURN QUERY EXECUTE $q$
45836
+ WITH lines AS (
45837
+ SELECT l.item_id, l.product_id, lower(btrim(coalesce(p.name, l.name))) AS pname
45838
+ FROM public.v_financial_invoice_lines l
45839
+ LEFT JOIN public.products p ON p.id = l.product_id
45840
+ WHERE l.invoice_id = $2 AND l.item_cancelled_at IS NULL
45841
+ ), entries AS (
45842
+ SELECT m.product_id, m.source_location_id AS location_id, sum(m.quantity) AS qty,
45843
+ min(m.movement_date)::date AS entered_on, lower(btrim(p.name)) AS pname
45844
+ FROM public.plg_inventory_stock_movements m
45845
+ LEFT JOIN public.products p ON p.id = m.product_id
45846
+ WHERE m.tenant_id = $1 AND m.kind = 'in'
45847
+ AND (m.reason = 'Entrada via DANFE NF-e ' || $3 OR m.document_number = 'NF-e ' || $3)
45848
+ GROUP BY m.product_id, m.source_location_id, p.name
45849
+ ), by_product AS (
45850
+ SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.product_id = l.product_id
45851
+ ), by_name AS (
45852
+ SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.pname = l.pname
45853
+ WHERE l.item_id NOT IN (SELECT item_id FROM by_product)
45854
+ AND e.product_id NOT IN (SELECT product_id FROM by_product)
45855
+ ), rest_l AS (
45856
+ SELECT l.item_id FROM lines l
45857
+ WHERE l.item_id NOT IN (SELECT item_id FROM by_product UNION SELECT item_id FROM by_name)
45858
+ ), rest_e AS (
45859
+ SELECT e.* FROM entries e
45860
+ WHERE e.product_id NOT IN (SELECT product_id FROM by_product UNION SELECT product_id FROM by_name)
45861
+ ), by_elimination AS (
45862
+ -- The V1 sometimes booked a note's entry under another product record.
45863
+ -- One line and one product left over on the same note: they are the pair.
45864
+ SELECT l.item_id, e.* FROM rest_l l CROSS JOIN rest_e e
45865
+ WHERE (SELECT count(*) FROM rest_l) = 1 AND (SELECT count(DISTINCT product_id) FROM rest_e) = 1
45866
+ ), matched AS (
45867
+ SELECT * FROM by_product UNION ALL SELECT * FROM by_name UNION ALL SELECT * FROM by_elimination
45868
+ )
45869
+ SELECT m.item_id, m.product_id, m.location_id, m.qty,
45870
+ greatest(least(m.qty, coalesce((SELECT sum(sp.quantity) FROM public.plg_inventory_stock_positions sp
45871
+ WHERE sp.tenant_id = $1 AND sp.product_id = m.product_id
45872
+ AND sp.stock_location_id = m.location_id), 0)), 0),
45873
+ m.entered_on
45874
+ FROM matched m
45875
+ $q$ USING v_tenant, p_invoice_id, v_nf;
45876
+ END $$;
45877
+ REVOKE ALL ON FUNCTION public.financial_invoice_stock_entries(uuid) FROM PUBLIC;
45878
+ GRANT EXECUTE ON FUNCTION public.financial_invoice_stock_entries(uuid) TO authenticated, service_role;
45879
+
45880
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45881
+ -- 7. Cancelar \u2014 itens ou a conta inteira
45882
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
45883
+ -- p_item_ids vazio/nulo = todos. p_stock = [{item_id, destination: 'devolver'
45884
+ -- | 'perda' | 'manter', note}] \u2014 obrigat\xF3rio para cada linha que entrou no
45885
+ -- estoque; NULL s\xF3 pelo caminho antigo (financial_cancel_invoice), que nunca
45886
+ -- perguntou e continua sem mexer no estoque.
45887
+
45888
+ CREATE OR REPLACE FUNCTION public.financial_cancel_invoice_items(
45889
+ p_invoice_id uuid,
45890
+ p_item_ids uuid[] DEFAULT NULL,
45891
+ p_reason_id uuid DEFAULT NULL,
45892
+ p_detail text DEFAULT NULL,
45893
+ p_stock jsonb DEFAULT '[]'::jsonb,
45894
+ p_idempotency_key text DEFAULT NULL
45895
+ ) RETURNS jsonb
45896
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
45897
+ DECLARE
45898
+ v_tenant uuid := public.plg_financial_require_tenant();
45899
+ v_inv public.plg_financial_invoices%ROWTYPE;
45900
+ v_reason text;
45901
+ v_rname text;
45902
+ v_live uuid[];
45903
+ v_sel uuid[];
45904
+ v_whole boolean;
45905
+ v_amount numeric := 0;
45906
+ v_cut numeric;
45907
+ v_take numeric;
45908
+ v_credit numeric := 0;
45909
+ v_paid numeric;
45910
+ v_inst record;
45911
+ v_alloc record;
45912
+ v_entry record;
45913
+ v_dest jsonb;
45914
+ v_dest_k text;
45915
+ v_stock jsonb := '[]'::jsonb;
45916
+ v_op jsonb;
45917
+ v_ops jsonb;
45918
+ v_person uuid;
45919
+ v_cancel_acc uuid;
45920
+ v_mov uuid;
45921
+ v_log uuid;
45922
+ v_nf text;
45923
+ BEGIN
45924
+ SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant FOR UPDATE;
45925
+ IF NOT FOUND THEN
45926
+ RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';
45927
+ END IF;
45928
+ PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);
45929
+
45930
+ IF p_idempotency_key IS NOT NULL THEN
45931
+ SELECT c.id INTO v_log FROM public.plg_financial_cancellations c
45932
+ WHERE c.tenant_id = v_tenant AND c.idempotency_key = p_idempotency_key;
45933
+ IF v_log IS NOT NULL THEN
45934
+ RETURN (SELECT jsonb_build_object('cancellation_id', c.id, 'invoice_id', c.invoice_id, 'whole', c.whole,
45935
+ 'amount', c.amount, 'credit_amount', c.credit_amount, 'replayed', true)
45936
+ FROM public.plg_financial_cancellations c WHERE c.id = v_log);
45937
+ END IF;
45938
+ END IF;
45939
+ IF v_inv.cancelled_at IS NOT NULL THEN
45940
+ RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % is already cancelled', v_inv.number USING ERRCODE = '55000';
45941
+ END IF;
45942
+
45943
+ -- O motivo: da lista, com o detalhe ao lado \u2014 \xABOutros \u2014 nota devolvida\xBB.
45944
+ IF p_reason_id IS NOT NULL THEN
45945
+ SELECT r.name INTO v_rname FROM public.plg_financial_cancellation_reasons r
45946
+ WHERE r.id = p_reason_id AND r.tenant_id = v_tenant;
45947
+ IF v_rname IS NULL THEN
45948
+ RAISE EXCEPTION 'financial_cancel_invoice_items: reason % not found', p_reason_id USING ERRCODE = 'P0002';
45949
+ END IF;
45950
+ END IF;
45951
+ v_reason := nullif(concat_ws(' \u2014 ', v_rname, nullif(btrim(coalesce(p_detail, '')), '')), '');
45952
+ IF v_reason IS NULL THEN
45953
+ RAISE EXCEPTION 'financial_cancel_invoice_items: a reason is required' USING ERRCODE = '22023';
45954
+ END IF;
45955
+
45956
+ SELECT coalesce(array_agg(l.item_id), '{}') INTO v_live
45957
+ FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_cancelled_at IS NULL;
45958
+ IF p_item_ids IS NULL OR cardinality(p_item_ids) = 0 THEN
45959
+ v_sel := v_live;
45960
+ ELSE
45961
+ IF EXISTS (SELECT 1 FROM unnest(p_item_ids) x WHERE x <> ALL (v_live)) THEN
45962
+ RAISE EXCEPTION 'financial_cancel_invoice_items: an item is not a live line of invoice %', v_inv.number USING ERRCODE = '22023';
45963
+ END IF;
45964
+ SELECT array_agg(DISTINCT x) INTO v_sel FROM unnest(p_item_ids) x;
45965
+ END IF;
45966
+ v_whole := NOT EXISTS (SELECT 1 FROM unnest(v_live) x WHERE x <> ALL (v_sel));
45967
+
45968
+ -- Quanto: a soma das linhas; a conta inteira cancela o que a conta vale (o
45969
+ -- V1 tem conta cujas linhas n\xE3o somam o total, e o total \xE9 o que se deve).
45970
+ IF v_whole THEN
45971
+ v_amount := v_inv.total;
45972
+ ELSE
45973
+ SELECT round(coalesce(sum(l.total), 0), 2) INTO v_amount
45974
+ FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_id = ANY (v_sel);
45975
+ v_amount := least(v_amount, v_inv.total);
45976
+ END IF;
45977
+
45978
+ PERFORM set_config('financial.amend', 'on', true);
45979
+
45980
+ -- \u2500\u2500 a mercadoria \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
45981
+ IF p_stock IS NOT NULL THEN
45982
+ FOR v_entry IN
45983
+ SELECT e.* FROM public.financial_invoice_stock_entries(p_invoice_id) e
45984
+ WHERE e.item_id = ANY (v_sel) ORDER BY e.item_id, e.entered_on DESC
45985
+ LOOP
45986
+ SELECT d INTO v_dest FROM jsonb_array_elements(p_stock) d WHERE d ->> 'item_id' = v_entry.item_id::text LIMIT 1;
45987
+ v_dest_k := v_dest ->> 'destination';
45988
+ IF v_dest_k IS NULL OR v_dest_k NOT IN ('devolver', 'perda', 'manter') THEN
45989
+ RAISE EXCEPTION 'financial_cancel_invoice_items: choose what happens to the goods of item % (devolver | perda | manter)', v_entry.item_id
45990
+ USING ERRCODE = '22023', HINT = 'ESCOLHA_DESTINO';
45991
+ END IF;
45992
+ v_op := NULL;
45993
+ IF v_dest_k <> 'manter' AND v_entry.available > 0 THEN
45994
+ EXECUTE 'SELECT public.inventory_adjust($1, $2, $3, $4)' INTO v_op
45995
+ USING v_entry.location_id,
45996
+ jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
45997
+ 'product_id', v_entry.product_id, 'quantity', -v_entry.available,
45998
+ 'notes', nullif(btrim(coalesce(v_dest ->> 'note', '')), '')))),
45999
+ CASE v_dest_k WHEN 'devolver' THEN 'Devolu\xE7\xE3o ao fornecedor' ELSE 'Perda de mercadoria da compra' END,
46000
+ 'cancel:' || p_invoice_id || ':' || v_entry.item_id || ':' || v_entry.location_id
46001
+ || coalesce(':' || p_idempotency_key, '');
46002
+ END IF;
46003
+ v_stock := v_stock || jsonb_build_object(
46004
+ 'item_id', v_entry.item_id, 'product_id', v_entry.product_id, 'location_id', v_entry.location_id,
46005
+ 'destination', v_dest_k, 'entered', v_entry.entered,
46006
+ 'moved', CASE WHEN v_op IS NULL THEN 0 ELSE v_entry.available END,
46007
+ 'note', nullif(btrim(coalesce(v_dest ->> 'note', '')), ''),
46008
+ 'operation_id', v_op ->> 'operation_id');
46009
+ END LOOP;
46010
+ END IF;
46011
+
46012
+ -- \u2500\u2500 as linhas \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
46013
+ UPDATE public.items SET cancelled_at = now(), cancel_reason = v_reason
46014
+ WHERE id = ANY (v_sel) AND tenant_id = v_tenant AND cancelled_at IS NULL;
46015
+
46016
+ -- \u2500\u2500 o dinheiro \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
46017
+ IF v_whole THEN
46018
+ SELECT coalesce(sum(b.paid), 0) INTO v_paid FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;
46019
+ v_credit := v_paid;
46020
+ UPDATE public.plg_financial_invoices
46021
+ SET cancelled_at = now(), cancel_reason = v_reason, cancel_reason_id = p_reason_id,
46022
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true, 'at', now()))
46023
+ WHERE id = p_invoice_id;
46024
+ UPDATE public.plg_financial_invoice_installments
46025
+ SET cancelled_at = now(),
46026
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true))
46027
+ WHERE invoice_id = p_invoice_id AND cancelled_at IS NULL;
46028
+ UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()
46029
+ WHERE financial_invoice_id = p_invoice_id AND movement_kind = 'bill' AND status <> 'cancelled';
46030
+ ELSE
46031
+ v_cut := v_amount;
46032
+ -- 1\xBA a parte N\xC3O paga, da \xFAltima parcela para tr\xE1s.
46033
+ FOR v_inst IN
46034
+ SELECT i.id, i.amount, public.plg_financial_installment_paid(i.id) AS paid
46035
+ FROM public.plg_financial_invoice_installments i
46036
+ WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL
46037
+ ORDER BY i.number DESC
46038
+ LOOP
46039
+ EXIT WHEN v_cut <= 0;
46040
+ v_take := least(greatest(v_inst.amount - v_inst.paid, 0), v_cut);
46041
+ CONTINUE WHEN v_take <= 0;
46042
+ UPDATE public.plg_financial_invoice_installments
46043
+ SET amount = amount - v_take,
46044
+ cancelled_at = CASE WHEN amount - v_take <= 0 AND v_inst.paid = 0 THEN now() END,
46045
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))
46046
+ WHERE id = v_inst.id;
46047
+ v_cut := v_cut - v_take;
46048
+ END LOOP;
46049
+ -- 2\xBA o que sobrou sai do pago: o pagamento mais recente primeiro, e o que
46050
+ -- ele deixa de quitar vira cr\xE9dito.
46051
+ FOR v_inst IN
46052
+ SELECT i.id, i.amount FROM public.plg_financial_invoice_installments i
46053
+ WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL
46054
+ ORDER BY i.number DESC
46055
+ LOOP
46056
+ EXIT WHEN v_cut <= 0;
46057
+ FOR v_alloc IN
46058
+ SELECT a.id, a.amount FROM public.plg_financial_allocations a
46059
+ JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id
46060
+ WHERE a.installment_id = v_inst.id AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL
46061
+ ORDER BY m.payment_date DESC NULLS LAST, a.created_at DESC
46062
+ LOOP
46063
+ EXIT WHEN v_cut <= 0;
46064
+ v_take := least(v_alloc.amount, v_cut);
46065
+ IF v_take >= v_alloc.amount THEN
46066
+ DELETE FROM public.plg_financial_allocations WHERE id = v_alloc.id;
46067
+ ELSE
46068
+ UPDATE public.plg_financial_allocations SET amount = amount - v_take,
46069
+ metadata = metadata || jsonb_build_object('reduced_by_cancellation', v_take)
46070
+ WHERE id = v_alloc.id;
46071
+ END IF;
46072
+ UPDATE public.plg_financial_invoice_installments
46073
+ SET amount = amount - v_take,
46074
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))
46075
+ WHERE id = v_inst.id;
46076
+ v_credit := v_credit + v_take;
46077
+ v_cut := v_cut - v_take;
46078
+ END LOOP;
46079
+ END LOOP;
46080
+ UPDATE public.plg_financial_invoices
46081
+ SET total = greatest(total - v_amount, 0),
46082
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))
46083
+ WHERE id = p_invoice_id;
46084
+ PERFORM public.plg_financial_sync_mirror(i.id)
46085
+ FROM public.plg_financial_invoice_installments i WHERE i.invoice_id = p_invoice_id;
46086
+ END IF;
46087
+
46088
+ -- \u2500\u2500 o cr\xE9dito \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
46089
+ -- Fornecedor: ele passa a nos dever \u2014 d\xE9bito na conta dele. Cliente: n\xF3s
46090
+ -- passamos a dever a ele \u2014 cr\xE9dito na conta dele (como financial_cancel_paid_item).
46091
+ v_credit := round(v_credit, 2);
46092
+ IF v_credit > 0 THEN
46093
+ IF v_inv.counterparty_person_id IS NULL THEN
46094
+ RAISE EXCEPTION 'financial_cancel_invoice_items: % was paid and the invoice has no counterparty to hold the credit', v_credit
46095
+ USING ERRCODE = '55000';
46096
+ END IF;
46097
+ v_person := public.plg_financial_person_account(v_tenant, v_inv.counterparty_person_id);
46098
+ v_cancel_acc := public.plg_financial_system_account(v_tenant, 'cancellations');
46099
+ INSERT INTO public.plg_financial_movements
46100
+ (tenant_id, unit_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date,
46101
+ method, reason, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)
46102
+ VALUES
46103
+ (v_tenant, v_inv.unit_id, v_inv.id, CASE WHEN v_inv.direction = 'payable' THEN 'credit' ELSE 'debit' END,
46104
+ 'transfer', v_credit, v_credit, 'paid', current_date, current_date, 'credit', v_reason,
46105
+ jsonb_build_object('source', 'financial_cancel_invoice_items', 'credit_source', 'cancellation',
46106
+ 'cancelled_invoice_id', v_inv.id, 'cancelled_item_ids', to_jsonb(v_sel)),
46107
+ CASE WHEN p_idempotency_key IS NOT NULL THEN 'cancel-credit:' || p_idempotency_key END, auth.uid(),
46108
+ CASE WHEN v_inv.direction = 'payable' THEN v_person ELSE v_cancel_acc END,
46109
+ CASE WHEN v_inv.direction = 'payable' THEN v_cancel_acc ELSE v_person END)
46110
+ RETURNING id INTO v_mov;
46111
+ END IF;
46112
+
46113
+ INSERT INTO public.plg_financial_cancellations
46114
+ (tenant_id, unit_id, invoice_id, whole, item_ids, reason_id, reason, amount, credit_amount, credit_movement_id,
46115
+ stock, idempotency_key, created_by)
46116
+ VALUES (v_tenant, v_inv.unit_id, v_inv.id, v_whole, v_sel, p_reason_id, v_reason, v_amount, v_credit, v_mov,
46117
+ v_stock, p_idempotency_key, auth.uid())
46118
+ RETURNING id INTO v_log;
46119
+
46120
+ PERFORM public.plg_financial_audit(v_tenant,
46121
+ CASE WHEN v_whole THEN 'financial.invoice.cancel' ELSE 'financial.invoice.cancel_items' END,
46122
+ 'plg_financial_invoices', v_inv.id::text,
46123
+ jsonb_build_object('reason', v_reason, 'reason_id', p_reason_id, 'item_ids', to_jsonb(v_sel), 'amount', v_amount,
46124
+ 'credit_amount', v_credit, 'stock', v_stock, 'cancellation_id', v_log));
46125
+
46126
+ RETURN jsonb_build_object('cancellation_id', v_log, 'invoice_id', v_inv.id, 'whole', v_whole, 'amount', v_amount,
46127
+ 'credit_amount', v_credit, 'credit_movement_id', v_mov, 'stock', v_stock, 'replayed', false);
46128
+ END $$;
46129
+ REVOKE ALL ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) FROM PUBLIC;
46130
+ GRANT EXECUTE ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) TO authenticated, service_role;
46131
+
46132
+ -- O caminho antigo (cancelar em lote na lista) vira a conta inteira do novo,
46133
+ -- com o texto como detalhe e sem tocar no estoque.
46134
+ CREATE OR REPLACE FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb
46135
+ LANGUAGE plpgsql SECURITY DEFINER
46136
+ SET search_path TO ''
46137
+ AS $$
46138
+ DECLARE
46139
+ v_tenant uuid := public.plg_financial_require_tenant();
46140
+ v_res jsonb;
46141
+ BEGIN
46142
+ IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN
46143
+ RAISE EXCEPTION 'financial_cancel_invoice: a reason is required' USING ERRCODE = '22023';
46144
+ END IF;
46145
+ IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant AND cancelled_at IS NOT NULL) THEN
46146
+ RETURN jsonb_build_object('invoice_id', p_invoice_id, 'status', 'cancelled', 'replayed', true);
46147
+ END IF;
46148
+ v_res := public.financial_cancel_invoice_items(p_invoice_id, NULL, NULL, p_reason, NULL, p_idempotency_key);
46149
+ RETURN v_res || jsonb_build_object('status', 'cancelled');
46150
+ END $$;
46151
+
46152
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46153
+ -- 8. Pagar com o cr\xE9dito do fornecedor
46154
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46155
+ -- O saldo da conta da pessoa N\xC3O serve: o porte do V1 lan\xE7a os pagamentos a
46156
+ -- fornecedor contra essa mesma conta (R$ 102 mil na Artorius em 09-10-2026), e
46157
+ -- lido como cr\xE9dito ele inventaria dinheiro. Cr\xE9dito de fornecedor \xE9 s\xF3 o que
46158
+ -- um cancelamento criou, menos o que j\xE1 se pagou com ele. As pernas do
46159
+ -- pagamento j\xE1 estavam certas; muda s\xF3 a confer\xEAncia. Remendo no corpo vivo,
46160
+ -- como a fiscal-br/015: reescrever 134 linhas de mem\xF3ria \xE9 como se perde regra.
46161
+
46162
+ CREATE OR REPLACE FUNCTION public.plg_financial_supplier_credit(p_tenant uuid, p_person uuid)
46163
+ RETURNS numeric LANGUAGE sql SECURITY DEFINER SET search_path TO '' AS $$
46164
+ WITH acc AS (SELECT public.plg_financial_person_account(p_tenant, p_person) AS id)
46165
+ SELECT round(
46166
+ coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc
46167
+ WHERE m.tenant_id = p_tenant AND m.debit_account_id = acc.id AND m.status <> 'cancelled'
46168
+ AND m.reversed_by_id IS NULL AND m.metadata ->> 'credit_source' = 'cancellation'), 0)
46169
+ - coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc
46170
+ WHERE m.tenant_id = p_tenant AND m.credit_account_id = acc.id AND m.status <> 'cancelled'
46171
+ AND m.reversed_by_id IS NULL AND m.movement_kind = 'payment' AND m.method = 'credit'
46172
+ AND m.direction = 'debit'), 0), 2)
46173
+ $$;
46174
+ REVOKE ALL ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC;
46175
+ GRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;
46176
+
46177
+ CREATE OR REPLACE FUNCTION public.financial_supplier_credit(p_person_id uuid)
46178
+ RETURNS numeric LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
46179
+ DECLARE v_tenant uuid := public.plg_financial_require_tenant();
46180
+ BEGIN
46181
+ PERFORM public.plg_financial_require_permission('financial.read');
46182
+ RETURN public.plg_financial_supplier_credit(v_tenant, p_person_id);
46183
+ END $$;
46184
+ REVOKE ALL ON FUNCTION public.financial_supplier_credit(uuid) FROM PUBLIC;
46185
+ GRANT EXECUTE ON FUNCTION public.financial_supplier_credit(uuid) TO authenticated, service_role;
46186
+
46187
+ DO $m$
46188
+ DECLARE
46189
+ v_def text;
46190
+ v_antes text := 'SELECT balance INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';
46191
+ v_depois text := 'SELECT CASE WHEN v_invoice.direction = ''payable'' THEN public.plg_financial_supplier_credit(v_tenant, v_invoice.counterparty_person_id) ELSE balance END INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';
46192
+ BEGIN
46193
+ SELECT pg_get_functiondef(p.oid) INTO v_def
46194
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
46195
+ WHERE n.nspname = 'public' AND p.proname = 'financial_receive_payment';
46196
+ IF v_def IS NULL OR position('plg_financial_supplier_credit' in v_def) > 0 THEN RETURN; END IF;
46197
+ IF position(v_antes in v_def) = 0 THEN
46198
+ RAISE EXCEPTION 'financial/120: financial_receive_payment n\xE3o \xE9 o esperado \u2014 confira \xE0 m\xE3o';
46199
+ END IF;
46200
+ EXECUTE replace(v_def, v_antes, v_depois);
46201
+ END
46202
+ $m$;
46203
+
46204
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46205
+ -- 9. Juros, multa e desconto entram NA conta
46206
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46207
+ -- No V1 o boleto pago com atraso ganha a linha \xABMulta/Juros\xBB (plano 5.1.3) e
46208
+ -- a parcela passa a valer o que o banco cobrou. A concilia\xE7\xE3o do V2 criava
46209
+ -- uma conta avulsa s\xF3 com os juros, sem fornecedor e sem caminho de volta.
46210
+
46211
+ CREATE OR REPLACE FUNCTION public.financial_add_installment_charge(
46212
+ p_installment_id uuid,
46213
+ p_kind text,
46214
+ p_amount numeric,
46215
+ p_chart_account_id uuid DEFAULT NULL,
46216
+ p_description text DEFAULT NULL,
46217
+ p_idempotency_key text DEFAULT NULL
46218
+ ) RETURNS jsonb
46219
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
46220
+ DECLARE
46221
+ v_tenant uuid := public.plg_financial_require_tenant();
46222
+ v_inst public.plg_financial_invoice_installments%ROWTYPE;
46223
+ v_inv public.plg_financial_invoices%ROWTYPE;
46224
+ v_amount numeric := round(coalesce(p_amount, 0), 2);
46225
+ v_signed numeric;
46226
+ v_paid numeric;
46227
+ v_item uuid;
46228
+ v_own boolean;
46229
+ v_label text;
46230
+ BEGIN
46231
+ IF p_kind NOT IN ('interest', 'fine', 'discount') THEN
46232
+ RAISE EXCEPTION 'financial_add_installment_charge: kind must be interest | fine | discount' USING ERRCODE = '22023';
46233
+ END IF;
46234
+ IF v_amount <= 0 THEN
46235
+ RAISE EXCEPTION 'financial_add_installment_charge: amount must be positive' USING ERRCODE = '22023';
46236
+ END IF;
46237
+ SELECT * INTO v_inst FROM public.plg_financial_invoice_installments WHERE id = p_installment_id AND tenant_id = v_tenant FOR UPDATE;
46238
+ IF NOT FOUND THEN
46239
+ RAISE EXCEPTION 'financial_add_installment_charge: installment % not found', p_installment_id USING ERRCODE = 'P0002';
46240
+ END IF;
46241
+ SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = v_inst.invoice_id FOR UPDATE;
46242
+ PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);
46243
+ IF v_inst.cancelled_at IS NOT NULL OR v_inv.cancelled_at IS NOT NULL THEN
46244
+ RAISE EXCEPTION 'financial_add_installment_charge: installment is cancelled' USING ERRCODE = '55000';
46245
+ END IF;
46246
+ IF p_idempotency_key IS NOT NULL THEN
46247
+ SELECT i.id INTO v_item FROM public.items i
46248
+ WHERE i.tenant_id = v_tenant AND i.metadata ->> 'charge_idempotency_key' = p_idempotency_key;
46249
+ IF v_item IS NOT NULL THEN
46250
+ RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id, 'replayed', true);
46251
+ END IF;
46252
+ END IF;
46253
+
46254
+ v_signed := CASE WHEN p_kind = 'discount' THEN -v_amount ELSE v_amount END;
46255
+ v_paid := public.plg_financial_installment_paid(v_inst.id);
46256
+ IF v_inst.amount + v_signed < v_paid THEN
46257
+ RAISE EXCEPTION 'financial_add_installment_charge: the discount takes installment % below what was already paid (%)', v_inst.number, v_paid
46258
+ USING ERRCODE = '23514';
46259
+ END IF;
46260
+ v_label := coalesce(nullif(btrim(coalesce(p_description, '')), ''),
46261
+ CASE p_kind WHEN 'interest' THEN 'Juros por atraso' WHEN 'fine' THEN 'Multa por atraso' ELSE 'Desconto obtido' END);
46262
+
46263
+ PERFORM set_config('financial.amend', 'on', true);
46264
+
46265
+ -- A linha mora onde as outras moram: conta que veio de um pedido (o V1 todo)
46266
+ -- lista as linhas DO PEDIDO enquanto n\xE3o tiver nenhuma pr\xF3pria \u2014 p\xF4r a
46267
+ -- primeira na conta faria as outras sumirem da tela (v_financial_invoice_lines).
46268
+ v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_inv.id) OR v_inv.order_id IS NULL;
46269
+ INSERT INTO public.items (tenant_id, unit_id, invoice_id, order_id, name, description, quantity, unit_price, total,
46270
+ sort_order, chart_account_id, metadata)
46271
+ VALUES (v_tenant, v_inv.unit_id,
46272
+ CASE WHEN v_own THEN v_inv.id END, CASE WHEN v_own THEN NULL ELSE v_inv.order_id END,
46273
+ v_label, v_label, 1, v_signed, v_signed,
46274
+ 1000 + coalesce((SELECT count(*) FROM public.v_financial_invoice_lines l WHERE l.invoice_id = v_inv.id), 0),
46275
+ p_chart_account_id,
46276
+ jsonb_strip_nulls(jsonb_build_object('itemKind', 'other', 'charge', p_kind, 'installment_id', v_inst.id,
46277
+ 'source', 'financial_add_installment_charge',
46278
+ 'charge_idempotency_key', p_idempotency_key)))
46279
+ RETURNING id INTO v_item;
46280
+
46281
+ UPDATE public.plg_financial_invoice_installments
46282
+ SET amount = amount + v_signed,
46283
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))
46284
+ WHERE id = v_inst.id;
46285
+ UPDATE public.plg_financial_invoices
46286
+ SET total = total + v_signed,
46287
+ metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))
46288
+ WHERE id = v_inv.id;
46289
+ PERFORM public.plg_financial_sync_mirror(v_inst.id);
46290
+
46291
+ PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.charge', 'plg_financial_invoices', v_inv.id::text,
46292
+ jsonb_build_object('installment_id', v_inst.id, 'kind', p_kind, 'amount', v_signed, 'item_id', v_item,
46293
+ 'chart_account_id', p_chart_account_id));
46294
+ RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id,
46295
+ 'amount', v_signed, 'installment_amount', v_inst.amount + v_signed, 'replayed', false);
46296
+ END $$;
46297
+ REVOKE ALL ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) FROM PUBLIC;
46298
+ GRANT EXECUTE ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) TO authenticated, service_role;
46299
+
46300
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46301
+ -- 10. A sincronia do V1 respeita o V2 \u2014 e para de dobrar parcela
46302
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
46303
+ -- Duas mudan\xE7as no corpo vivo (que difere do arquivo 071/079):
46304
+ --
46305
+ -- a) parcela que o V2 cancelou ou emendou (\`v2_decision\`) n\xE3o volta a valer
46306
+ -- nem a ter o valor antigo quando o V1 regrava a linha;
46307
+ -- b) o V1 apaga e recria a parcela ao salvar a conta. Em conviv\xEAncia a
46308
+ -- exclus\xE3o n\xE3o atravessa, e a recriada ca\xEDa no \`unique_violation\` e
46309
+ -- ENTRAVA COMO N\xBA+1 \u2014 PAG-000330 com 5.656,40 + 5.656,48 para um aluguel
46310
+ -- de 5.656,48. Agora, se somar a nova estouraria o total da conta, ela \xE9
46311
+ -- a substituta: a antiga (migrada, sem baixa) sai cancelada e a nova
46312
+ -- herda o n\xFAmero. Duas parcelas vivas de mesmo n\xFAmero que CABEM no total
46313
+ -- (a fatura #1869) continuam entrando como antes.
46314
+
46315
+ CREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid, p_number integer, p_due date, p_amount numeric, p_cancelled boolean, p_sync boolean)
46316
+ RETURNS void
46317
+ LANGUAGE plpgsql
46318
+ SECURITY DEFINER
46319
+ SET search_path TO ''
46320
+ AS $function$
46321
+ DECLARE
46322
+ v_number integer := greatest(coalesce(p_number, 1), 1);
46323
+ v_cancel timestamptz := CASE WHEN coalesce(p_cancelled, false)
46324
+ THEN coalesce(p_due, current_date)::timestamptz END;
46325
+ v_old record;
46326
+ v_total numeric;
46327
+ v_live numeric;
46328
+ BEGIN
46329
+ IF p_invoice IS NULL THEN RETURN; END IF;
46330
+
46331
+ DELETE FROM public.plg_financial_invoice_installments i
46332
+ WHERE i.invoice_id = p_invoice
46333
+ AND i.id <> p_id
46334
+ AND NOT (i.metadata ? 'migration')
46335
+ AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)
46336
+ AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m
46337
+ WHERE m.financial_invoice_installment_id = i.id);
46338
+
46339
+ BEGIN
46340
+ INSERT INTO public.plg_financial_invoice_installments
46341
+ (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)
46342
+ VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due,
46343
+ greatest(coalesce(p_amount, 0), 0), v_cancel,
46344
+ jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))
46345
+ ON CONFLICT (id) DO UPDATE SET
46346
+ number = EXCLUDED.number,
46347
+ due_date = EXCLUDED.due_date,
46348
+ amount = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'
46349
+ THEN public.plg_financial_invoice_installments.amount ELSE EXCLUDED.amount END,
46350
+ cancelled_at = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'
46351
+ THEN public.plg_financial_invoice_installments.cancelled_at ELSE EXCLUDED.cancelled_at END,
46352
+ unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),
46353
+ metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;
46354
+ EXCEPTION
46355
+ WHEN unique_violation THEN
46356
+ SELECT i.id, i.amount INTO v_old
46357
+ FROM public.plg_financial_invoice_installments i
46358
+ WHERE i.invoice_id = p_invoice AND i.number = v_number AND i.id <> p_id
46359
+ AND i.cancelled_at IS NULL
46360
+ AND i.metadata ? 'migration' AND NOT (i.metadata ? 'v2_decision')
46361
+ AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id);
46362
+ SELECT f.total INTO v_total FROM public.plg_financial_invoices f WHERE f.id = p_invoice;
46363
+ SELECT coalesce(sum(i.amount), 0) INTO v_live FROM public.plg_financial_invoice_installments i
46364
+ WHERE i.invoice_id = p_invoice AND i.cancelled_at IS NULL;
46365
+
46366
+ IF v_old.id IS NOT NULL AND v_cancel IS NULL
46367
+ AND v_live + greatest(coalesce(p_amount, 0), 0) > coalesce(v_total, 0) + 0.01 THEN
46368
+ -- A substituta: a antiga sai do n\xFAmero (cancelada) e a nova entra nele.
46369
+ UPDATE public.plg_financial_invoice_installments
46370
+ SET cancelled_at = now(),
46371
+ number = (SELECT coalesce(max(x.number), 0) + 1 FROM public.plg_financial_invoice_installments x WHERE x.invoice_id = p_invoice),
46372
+ metadata = metadata || jsonb_build_object('superseded_by', p_id)
46373
+ WHERE id = v_old.id;
46374
+ INSERT INTO public.plg_financial_invoice_installments
46375
+ (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)
46376
+ VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due, greatest(coalesce(p_amount, 0), 0), NULL,
46377
+ jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'supersedes', v_old.id)))
46378
+ ON CONFLICT (id) DO NOTHING;
46379
+ PERFORM public.plg_financial_sync_mirror(v_old.id);
46380
+ ELSE
46381
+ INSERT INTO public.plg_financial_invoice_installments
46382
+ (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)
46383
+ SELECT p_id, p_tenant, p_unit, p_invoice, coalesce(max(i.number), 0) + 1, p_due,
46384
+ greatest(coalesce(p_amount, 0), 0), v_cancel,
46385
+ jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))
46386
+ FROM public.plg_financial_invoice_installments i
46387
+ WHERE i.invoice_id = p_invoice
46388
+ ON CONFLICT (id) DO NOTHING;
46389
+ END IF;
46390
+ END;
46391
+
46392
+ IF p_sync THEN
46393
+ PERFORM public.plg_financial_sync_mirror(p_id);
46394
+ END IF;
46395
+ END $function$;
46396
+ `;
46397
+ var MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO = `-- O terceiro motivo padr\xE3o \xE9 \xABSolicitado pelo estabelecimento\xBB, n\xE3o \xABpelo
46398
+ -- profissional\xBB (decis\xE3o do founder, 09-10-2026): numa conta a pagar quem pede
46399
+ -- o cancelamento \xE9 a casa, e o V1 herdou o r\xF3tulo da agenda de servi\xE7os.
46400
+ --
46401
+ -- Renomeia o semeado pela 120 s\xF3 onde ningu\xE9m o usou \u2014 um motivo j\xE1 gravado num
46402
+ -- cancelamento continua dizendo o que disse. Idempotente.
46403
+
46404
+ UPDATE public.plg_financial_cancellation_reasons r
46405
+ SET name = 'Solicitado pelo estabelecimento'
46406
+ WHERE r.name = 'Solicitado pelo profissional'
46407
+ AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellations c WHERE c.reason_id = r.id)
46408
+ AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons x
46409
+ WHERE x.tenant_id = r.tenant_id AND lower(x.name) = 'solicitado pelo estabelecimento');
46410
+
46411
+ CREATE OR REPLACE FUNCTION public.financial_cancellation_reasons()
46412
+ RETURNS SETOF public.plg_financial_cancellation_reasons
46413
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
46414
+ DECLARE v_tenant uuid := public.plg_financial_require_tenant();
46415
+ BEGIN
46416
+ PERFORM public.plg_financial_require_permission('financial.read');
46417
+ IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN
46418
+ INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)
46419
+ SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo estabelecimento']) n
46420
+ ON CONFLICT DO NOTHING;
46421
+ END IF;
46422
+ RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r
46423
+ WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;
46424
+ END $$;
46425
+ `;
46426
+ var MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU = `-- ============================================================================
46427
+ -- 122 \u2014 cancelling closes the doors 120 left open
46428
+ --
46429
+ -- 120 (cancel by item, with reason, stock and supplier credit) shipped three
46430
+ -- regressions the gates caught on main and that were live on the cluster:
46431
+ --
46432
+ -- 1. \`v_financial_invoice_lines\` was recreated WITHOUT \`security_invoker\`. A
46433
+ -- view without it runs as its owner and skips RLS, so every signed-in user
46434
+ -- could read every tenant's invoice lines. Every earlier definition of this
46435
+ -- view (010 \u2026 114) was security_invoker; this puts it back.
46436
+ --
46437
+ -- 2. \`plg_financial_supplier_credit(p_tenant, p_person)\` takes the tenant as an
46438
+ -- argument and was revoked only FROM PUBLIC. The pools' default privileges
46439
+ -- grant EXECUTE to \`authenticated\` on every new function, so any user could
46440
+ -- ask for any tenant's supplier credit. Only the definer wrappers
46441
+ -- (\`financial_supplier_credit\`, the payment check) call it, and they run as
46442
+ -- the owner, so closing it to clients changes nothing for them.
46443
+ --
46444
+ -- 3. Three money RPCs accept \`p_idempotency_key\` but did not claim it, so two
46445
+ -- concurrent calls with the same key could both pass the replay lookup and
46446
+ -- both write (#142). They now take the same advisory lock every other money
46447
+ -- RPC takes, right before that lookup. The bodies are patched in place, as
46448
+ -- 120 itself patched the payment check: copying 130 lines by hand is how a
46449
+ -- rule gets lost.
46450
+ --
46451
+ -- 1 and 2 were hot-fixed on klvfxzreepavcpyjiwla on 2026-10-09 with the same
46452
+ -- statements; this file is the record, and it is idempotent.
46453
+ -- ============================================================================
46454
+
46455
+ -- \u2500\u2500 1. the view reads with the caller's rights again \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
46456
+ ALTER VIEW public.v_financial_invoice_lines SET (security_invoker = true);
46457
+
46458
+ -- \u2500\u2500 2. the tenant-taking helper is not a client door \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
46459
+ REVOKE EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC, anon, authenticated;
46460
+ GRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;
46461
+
46462
+ -- \u2500\u2500 3. an idempotency key is claimed before it is looked up \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
46463
+ DO $patch$
46464
+ DECLARE
46465
+ v_fn regprocedure;
46466
+ v_def text;
46467
+ v_anchor text;
46468
+ v_claim constant text := 'PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);';
46469
+ BEGIN
46470
+ FOR v_fn, v_anchor IN
46471
+ SELECT f::regprocedure, a FROM (VALUES
46472
+ -- the replay lookup is the first thing that reads the key
46473
+ ('public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text)', ' IF p_idempotency_key IS NOT NULL THEN'),
46474
+ ('public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text)', ' IF p_idempotency_key IS NOT NULL THEN'),
46475
+ -- this one replays on "already cancelled" before it delegates, so the
46476
+ -- claim goes ahead of that check; the delegate re-takes the same lock,
46477
+ -- which a transaction-scoped advisory lock allows
46478
+ ('public.financial_cancel_invoice(uuid, text, text)', ' IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id')
46479
+ ) AS t(f, a)
46480
+ LOOP
46481
+ v_def := pg_get_functiondef(v_fn);
46482
+ IF position('plg_financial_claim_idempotency' in v_def) > 0 THEN
46483
+ CONTINUE; -- already claims (a replay, or a later rewrite)
46484
+ END IF;
46485
+ IF position(v_anchor in v_def) = 0 THEN
46486
+ RAISE EXCEPTION '122: % no longer has the line the claim goes before (%); patch it by hand', v_fn, v_anchor;
46487
+ END IF;
46488
+ -- first occurrence only: the anchor is the replay lookup, not every later use of the key
46489
+ EXECUTE overlay(v_def PLACING ' ' || v_claim || E'\\n' || v_anchor
46490
+ FROM position(v_anchor in v_def) FOR length(v_anchor));
46491
+ END LOOP;
46492
+ END $patch$;
46493
+ `;
45330
46494
  var MIGRATIONS = [
45331
46495
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
45332
46496
  { id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
@@ -45447,7 +46611,10 @@ var MIGRATIONS = [
45447
46611
  { id: "116_um_estorno_nao_se_estorna", sql: MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA },
45448
46612
  { id: "117_copying_lines_is_an_internal_step", sql: MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP },
45449
46613
  { id: "118_gift_card_found_by_exact_code", sql: MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE },
45450
- { id: "119_an_unpaid_legacy_bill_can_be_rescheduled", sql: MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED }
46614
+ { id: "119_an_unpaid_legacy_bill_can_be_rescheduled", sql: MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED },
46615
+ { id: "120_cancelar_e_por_item_com_motivo_e_destino", sql: MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO },
46616
+ { id: "121_o_motivo_e_do_estabelecimento", sql: MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO },
46617
+ { id: "122_o_cancelamento_fecha_as_portas_que_abriu", sql: MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU }
45451
46618
  ];
45452
46619
  var STANDALONE_CONFIG = resolveConfig();
45453
46620
  registerTranslations(financialLocales);
@@ -45457,8 +46624,8 @@ function ReceivePaymentModal({
45457
46624
  onClose,
45458
46625
  onPaid
45459
46626
  }) {
45460
- const provider = React25__default.useMemo(() => createSafeFinancialProvider(), []);
45461
- const store6 = React25__default.useMemo(() => createFinancialStore(provider), [provider]);
46627
+ const provider = React26__default.useMemo(() => createSafeFinancialProvider(), []);
46628
+ const store6 = React26__default.useMemo(() => createFinancialStore(provider), [provider]);
45462
46629
  return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
45463
46630
  PaymentModal,
45464
46631
  {
@@ -45476,7 +46643,7 @@ function createFinancialSettingsPlugin(options) {
45476
46643
  ...financialRegistries.filter((r) => RECEIVING_REGISTRIES.includes(r.id)),
45477
46644
  ...options?.extraRegistries ?? []
45478
46645
  ];
45479
- const SettingsTab = () => React25__default.createElement(PluginSettingsPanel, {
46646
+ const SettingsTab = () => React26__default.createElement(PluginSettingsPanel, {
45480
46647
  title: "Pagamentos",
45481
46648
  registries,
45482
46649
  hostPluginId: "financial",
@@ -45533,27 +46700,27 @@ function ClientAccountPanel({
45533
46700
  currency = BRL
45534
46701
  }) {
45535
46702
  const provider = useFinancialProvider();
45536
- const [account, setAccount] = React25__default.useState(null);
45537
- const [busy, setBusy] = React25__default.useState(false);
45538
- const [viewInvoiceId, setViewInvoiceId] = React25__default.useState(null);
45539
- const [comprando, setComprando] = React25__default.useState(false);
45540
- const [buscaServico, setBuscaServico] = React25__default.useState("");
45541
- const [catalogo, setCatalogo] = React25__default.useState([]);
45542
- const [escolhidos, setEscolhidos] = React25__default.useState([]);
45543
- const [comprandoBusy, setComprandoBusy] = React25__default.useState(false);
45544
- const [erroCompra, setErroCompra] = React25__default.useState(null);
45545
- const load = React25__default.useCallback(async () => {
46703
+ const [account, setAccount] = React26__default.useState(null);
46704
+ const [busy, setBusy] = React26__default.useState(false);
46705
+ const [viewInvoiceId, setViewInvoiceId] = React26__default.useState(null);
46706
+ const [comprando, setComprando] = React26__default.useState(false);
46707
+ const [buscaServico, setBuscaServico] = React26__default.useState("");
46708
+ const [catalogo, setCatalogo] = React26__default.useState([]);
46709
+ const [escolhidos, setEscolhidos] = React26__default.useState([]);
46710
+ const [comprandoBusy, setComprandoBusy] = React26__default.useState(false);
46711
+ const [erroCompra, setErroCompra] = React26__default.useState(null);
46712
+ const load = React26__default.useCallback(async () => {
45546
46713
  if (!personId) return;
45547
46714
  setAccount(await provider.getClientAccount(personId));
45548
46715
  }, [personId, provider]);
45549
- React25__default.useEffect(() => {
46716
+ React26__default.useEffect(() => {
45550
46717
  void load();
45551
46718
  }, [load]);
45552
46719
  const viewInvoice = account?.invoices.find((i) => i.id === viewInvoiceId) ?? null;
45553
46720
  const viewEditable = invoiceOpensForEditing(viewInvoice?.status);
45554
46721
  const forThis = account?.invoices.filter((i) => orderId && i.orderId === orderId) ?? [];
45555
46722
  const hasForThis = forThis.length > 0;
45556
- React25__default.useEffect(() => {
46723
+ React26__default.useEffect(() => {
45557
46724
  if (!comprando) return;
45558
46725
  let vivo = true;
45559
46726
  const t2 = setTimeout(async () => {
@@ -45828,8 +46995,8 @@ function ClientAccountPanel({
45828
46995
  ] });
45829
46996
  }
45830
46997
  function ClientAccountInline(props) {
45831
- const provider = React25__default.useMemo(() => createSafeFinancialProvider(), []);
45832
- const store6 = React25__default.useMemo(() => createFinancialStore(provider), [provider]);
46998
+ const provider = React26__default.useMemo(() => createSafeFinancialProvider(), []);
46999
+ const store6 = React26__default.useMemo(() => createFinancialStore(provider), [provider]);
45833
47000
  return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(ClientAccountPanel, { ...props }) });
45834
47001
  }
45835
47002
  function ClientAccountModal({
@@ -45877,11 +47044,11 @@ function createFinancialPlugin(options) {
45877
47044
  const settingsRegistries = registries.filter((r) => r.id !== "bank-accounts");
45878
47045
  const AccountsRegistryPage = createCrudPage(financialAccountEntity, { feature: "financial" });
45879
47046
  AccountsRegistryPage.displayName = "FinancialAccountsRegistryPage";
45880
- const PersonStatementBound = (props) => React25__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
47047
+ const PersonStatementBound = (props) => React26__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
45881
47048
  PersonStatementBound.displayName = "FinancialPersonStatement";
45882
- const SupplierFinancialSettingsBound = (props) => React25__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
47049
+ const SupplierFinancialSettingsBound = (props) => React26__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
45883
47050
  SupplierFinancialSettingsBound.displayName = "FinancialSupplierSettings";
45884
- const PageComponent = () => React25__default.createElement(FinancialPage, { config, provider, store: store6, registries });
47051
+ const PageComponent = () => React26__default.createElement(FinancialPage, { config, provider, store: store6, registries });
45885
47052
  const AgentMount = createFinancialAgentMount({ config, provider });
45886
47053
  return {
45887
47054
  id: "financial",
@@ -45939,7 +47106,7 @@ function createFinancialPlugin(options) {
45939
47106
  // tem — lá é um ícone no cabeçalho, de qualquer tela.
45940
47107
  headerControls: config.modules.cashRegisters ? [{
45941
47108
  id: "financial.cash-session",
45942
- component: (() => React25__default.createElement(CashSessionHeaderControl, { config, provider, store: store6 })),
47109
+ component: (() => React26__default.createElement(CashSessionHeaderControl, { config, provider, store: store6 })),
45943
47110
  order: 90,
45944
47111
  permission: { feature: "financial", action: "read" }
45945
47112
  }] : [],
@@ -46170,9 +47337,9 @@ function createFinancialPlugin(options) {
46170
47337
  // lado, e a corrida de "Plugins" não é partida ao meio.
46171
47338
  group: "Financeiro",
46172
47339
  component: (() => {
46173
- const FinancialSettingsTab = () => React25__default.createElement(PluginSettingsPanel, {
47340
+ const FinancialSettingsTab = () => React26__default.createElement(PluginSettingsPanel, {
46174
47341
  title: "Financial Settings",
46175
- generalSettings: React25__default.createElement(FinancialGeneralSettings, { config }),
47342
+ generalSettings: React26__default.createElement(FinancialGeneralSettings, { config }),
46176
47343
  registries: settingsRegistries,
46177
47344
  hostPluginId: "financial",
46178
47345
  routeBase: "/settings/financial"
@@ -46197,7 +47364,7 @@ function createFinancialPlugin(options) {
46197
47364
  icon: "HandCoins",
46198
47365
  group: "Financeiro",
46199
47366
  component: (() => {
46200
- const CommissionRulesTab = () => React25__default.createElement(CommissionRulesView, { provider });
47367
+ const CommissionRulesTab = () => React26__default.createElement(CommissionRulesView, { provider });
46201
47368
  CommissionRulesTab.displayName = "CommissionRulesTab";
46202
47369
  return CommissionRulesTab;
46203
47370
  })(),