@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/components/CancelInvoiceDialog.d.ts +14 -0
- package/dist/components/CancelInvoiceDialog.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +11 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +1622 -455
- package/dist/index.js.map +1 -1
- package/dist/lib/reconcile.d.ts +8 -5
- package/dist/lib/reconcile.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +4 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
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,
|
|
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 =
|
|
911
|
+
const live = React26.useRef(onOpenDraft);
|
|
912
912
|
live.current = onOpenDraft;
|
|
913
|
-
const contribution =
|
|
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] =
|
|
1408
|
-
const [bankAccountId, setBankAccountId] =
|
|
1409
|
-
const [draft, setDraft] =
|
|
1410
|
-
const [context, setContext] =
|
|
1411
|
-
const [companyName, setCompanyName] =
|
|
1412
|
-
const [companyDocument, setCompanyDocument] =
|
|
1413
|
-
const [lines, setLines] =
|
|
1414
|
-
const [companyUnsaved, setCompanyUnsaved] =
|
|
1415
|
-
const [loadingContext, setLoadingContext] =
|
|
1416
|
-
const [busy, setBusy] =
|
|
1417
|
-
const [error, setError] =
|
|
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 =
|
|
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 =
|
|
1448
|
-
const batches =
|
|
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 =
|
|
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 =
|
|
3954
|
+
const live = React26.useRef(draft);
|
|
3955
3955
|
live.current = draft;
|
|
3956
|
-
const lookup =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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] =
|
|
4779
|
-
const [q, setQ] =
|
|
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] =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 [
|
|
5310
|
-
const [
|
|
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
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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__ */
|
|
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:
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
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.
|
|
6173
|
-
value: item.
|
|
6174
|
-
displayValue:
|
|
6175
|
-
placeholder: t2("financial.invoiceForm.
|
|
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.
|
|
6178
|
-
onSearch: (q) => filterByCodeOrName(
|
|
6179
|
-
id:
|
|
6180
|
-
label:
|
|
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, {
|
|
6184
|
-
onManage: canManageFinancial ? () => adminNavigateTo(
|
|
6185
|
-
manageLabel: t2("financial.invoiceForm.
|
|
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
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
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
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
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 [
|
|
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
|
-
|
|
6625
|
-
|
|
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
|
|
6679
|
-
const
|
|
6680
|
-
const
|
|
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
|
-
...
|
|
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
|
|
6719
|
-
...onEdit ? [{ id: "edit", label: t2("financial.invoice.edit"), icon: Pencil, intent: "primary", onSelect: () => onEdit() }] : [],
|
|
6720
|
-
|
|
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:
|
|
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
|
-
|
|
6994
|
-
|
|
7167
|
+
cancelTarget !== null && /* @__PURE__ */ jsx(
|
|
7168
|
+
CancelInvoiceDialog,
|
|
6995
7169
|
{
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
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 =
|
|
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 =
|
|
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] =
|
|
8435
|
-
const [to, setTo] =
|
|
8436
|
-
const [preset, setPreset] =
|
|
8437
|
-
const [running, setRunning] =
|
|
8438
|
-
const [lastRun, setLastRun] =
|
|
8439
|
-
const [generation, setGeneration] =
|
|
8440
|
-
const [busy, setBusy] =
|
|
8441
|
-
const [error, setError] =
|
|
8442
|
-
const loadGeneration =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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] =
|
|
8956
|
-
const [unitId, setUnitId] =
|
|
8957
|
-
const [year, setYear] =
|
|
8958
|
-
const [rows, setRows] =
|
|
8959
|
-
const [editing, setEditing] =
|
|
8960
|
-
const [error, setError] =
|
|
8961
|
-
|
|
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 =
|
|
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
|
-
|
|
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] =
|
|
9112
|
-
const [sup, setSup] =
|
|
9113
|
-
const [hyper, setHyper] =
|
|
9114
|
-
const [saving, setSaving] =
|
|
9115
|
-
const [error, setError] =
|
|
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, {
|
|
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] =
|
|
9207
|
-
const [cards, setCards] =
|
|
9208
|
-
const [error, setError] =
|
|
9209
|
-
const [issuing, setIssuing] =
|
|
9210
|
-
const [issued, setIssued] =
|
|
9211
|
-
const [activating, setActivating] =
|
|
9212
|
-
const [search, setSearch] =
|
|
9213
|
-
const [state, setState] =
|
|
9214
|
-
const [minValue, setMinValue] =
|
|
9215
|
-
const [maxValue, setMaxValue] =
|
|
9216
|
-
const [from, setFrom] =
|
|
9217
|
-
const [to, setTo] =
|
|
9218
|
-
const money4 =
|
|
9219
|
-
const seq =
|
|
9220
|
-
const load =
|
|
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
|
-
|
|
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
|
-
|
|
9406
|
+
React26__default.useEffect(() => {
|
|
9251
9407
|
if (enabled) void load();
|
|
9252
9408
|
}, [enabled, load]);
|
|
9253
|
-
const columns =
|
|
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] =
|
|
9435
|
-
const [amount, setAmount] =
|
|
9436
|
-
const [expiresAt, setExpiresAt] =
|
|
9437
|
-
const [note, setNote] =
|
|
9438
|
-
const [saving, setSaving] =
|
|
9439
|
-
const [error, setError] =
|
|
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,
|
|
9538
|
-
return year && month &&
|
|
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
|
|
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,
|
|
9846
|
-
return year && month &&
|
|
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,
|
|
10808
|
-
return year && month &&
|
|
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 =
|
|
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 =
|
|
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,
|
|
11388
|
-
return year && month &&
|
|
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] =
|
|
11416
|
-
const [search, setSearch] =
|
|
11417
|
-
const [status, setStatus] =
|
|
11418
|
-
const [changing, setChanging] =
|
|
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 =
|
|
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
|
-
|
|
11604
|
+
React26__default.useEffect(() => {
|
|
11431
11605
|
void load();
|
|
11432
11606
|
}, [load]);
|
|
11433
|
-
const filtered =
|
|
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 =
|
|
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 =
|
|
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] =
|
|
11563
|
-
const [loaded, setLoaded] =
|
|
11564
|
-
const [contact, setContact] =
|
|
11565
|
-
const [counterpartyAccounting, setCounterpartyAccounting] =
|
|
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 =
|
|
11571
|
-
const counterpartyLookup =
|
|
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 =
|
|
11579
|
-
const [items, setItems] =
|
|
11580
|
-
const [expandedItemId, setExpandedItemId] =
|
|
11581
|
-
const [addingItem, setAddingItem] =
|
|
11582
|
-
const [frequency, setFrequency] =
|
|
11583
|
-
const [dueDay, setDueDay] =
|
|
11584
|
-
const [startsOn, setStartsOn] =
|
|
11585
|
-
const [endsOn, setEndsOn] =
|
|
11586
|
-
const [unitId, setUnitId] =
|
|
11587
|
-
const [availableLocations, setAvailableLocations] =
|
|
11588
|
-
const [notes, setNotes] =
|
|
11589
|
-
const [isActive, setIsActive] =
|
|
11590
|
-
const [accounts, setAccounts] =
|
|
11591
|
-
const [costCenters, setCostCenters] =
|
|
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] =
|
|
11594
|
-
const [ending, setEnding] =
|
|
11595
|
-
const [error, setError] =
|
|
11596
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
11685
|
-
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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 =
|
|
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] =
|
|
17005
|
-
const [loadError, setLoadError] =
|
|
17006
|
-
const [expanded, setExpanded] =
|
|
17007
|
-
const [query, setQuery] =
|
|
17008
|
-
const [showInactive, setShowInactive] =
|
|
17009
|
-
const [editing, setEditing] =
|
|
17010
|
-
const [confirmDelete, setConfirmDelete] =
|
|
17011
|
-
const [busy, setBusy] =
|
|
17012
|
-
const initialized =
|
|
17013
|
-
const reload =
|
|
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
|
-
|
|
17256
|
+
React26__default.useEffect(() => {
|
|
17029
17257
|
void reload();
|
|
17030
17258
|
}, [reload]);
|
|
17031
|
-
const shown =
|
|
17259
|
+
const shown = React26__default.useMemo(
|
|
17032
17260
|
() => (nodes ?? []).filter((n) => showInactive || n.isActive),
|
|
17033
17261
|
[nodes, showInactive]
|
|
17034
17262
|
);
|
|
17035
|
-
const tree =
|
|
17036
|
-
const fullTree =
|
|
17037
|
-
const search =
|
|
17038
|
-
const groupIds =
|
|
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 =
|
|
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) =>
|
|
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 }) =>
|
|
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
|
-
|
|
18632
|
+
React26__default.useEffect(() => {
|
|
18405
18633
|
void fetchCashSessions?.();
|
|
18406
18634
|
}, [fetchCashSessions]);
|
|
18407
|
-
|
|
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] =
|
|
20429
|
-
const [description, setDescription] =
|
|
20430
|
-
const [priority, setPriority] =
|
|
20431
|
-
const [isActive, setIsActive] =
|
|
20432
|
-
const [isDefault, setIsDefault] =
|
|
20433
|
-
const [saving, setSaving] =
|
|
20434
|
-
const [error, setError] =
|
|
20435
|
-
const initial =
|
|
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] =
|
|
20447
|
-
const [categories, setCategories] =
|
|
20448
|
-
const [professionals, setProfessionals] =
|
|
20449
|
-
const [units, setUnits] =
|
|
20450
|
-
const [weekdays, setWeekdays] =
|
|
20451
|
-
const [ranges, setRanges] =
|
|
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, {
|
|
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] =
|
|
20922
|
+
const [functions, setFunctions] = React26__default.useState(
|
|
20695
20923
|
rule.functions.length ? rule.functions : [blankFunction(0)]
|
|
20696
20924
|
);
|
|
20697
|
-
const [saving, setSaving] =
|
|
20698
|
-
const [error, setError] =
|
|
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, {
|
|
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] =
|
|
20983
|
-
const [catalogs, setCatalogs] =
|
|
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] =
|
|
20990
|
-
const [funnel, setFunnel] =
|
|
20991
|
-
const [editing, setEditing] =
|
|
20992
|
-
const [functionsFor, setFunctionsFor] =
|
|
20993
|
-
const [deleting, setDeleting] =
|
|
20994
|
-
const load =
|
|
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
|
-
|
|
21236
|
+
React26__default.useEffect(() => {
|
|
21009
21237
|
void load();
|
|
21010
21238
|
}, [load]);
|
|
21011
21239
|
const funnelActive = Object.values(funnel).some(Boolean);
|
|
21012
|
-
const visible =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
45461
|
-
const store6 =
|
|
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 = () =>
|
|
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] =
|
|
45537
|
-
const [busy, setBusy] =
|
|
45538
|
-
const [viewInvoiceId, setViewInvoiceId] =
|
|
45539
|
-
const [comprando, setComprando] =
|
|
45540
|
-
const [buscaServico, setBuscaServico] =
|
|
45541
|
-
const [catalogo, setCatalogo] =
|
|
45542
|
-
const [escolhidos, setEscolhidos] =
|
|
45543
|
-
const [comprandoBusy, setComprandoBusy] =
|
|
45544
|
-
const [erroCompra, setErroCompra] =
|
|
45545
|
-
const load =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
45832
|
-
const store6 =
|
|
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) =>
|
|
47047
|
+
const PersonStatementBound = (props) => React26__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
45881
47048
|
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
45882
|
-
const SupplierFinancialSettingsBound = (props) =>
|
|
47049
|
+
const SupplierFinancialSettingsBound = (props) => React26__default.createElement(SupplierFinancialSettings, { person: props.item, provider });
|
|
45883
47050
|
SupplierFinancialSettingsBound.displayName = "FinancialSupplierSettings";
|
|
45884
|
-
const PageComponent = () =>
|
|
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: (() =>
|
|
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 = () =>
|
|
47340
|
+
const FinancialSettingsTab = () => React26__default.createElement(PluginSettingsPanel, {
|
|
46174
47341
|
title: "Financial Settings",
|
|
46175
|
-
generalSettings:
|
|
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 = () =>
|
|
47367
|
+
const CommissionRulesTab = () => React26__default.createElement(CommissionRulesView, { provider });
|
|
46201
47368
|
CommissionRulesTab.displayName = "CommissionRulesTab";
|
|
46202
47369
|
return CommissionRulesTab;
|
|
46203
47370
|
})(),
|