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