@fayz-ai/plugin-inventory 0.12.9 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/InventoryGeneralSettings.d.ts +15 -1
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +10 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +4016 -100
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +14 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +8 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InventoryPage.d.ts +1 -1
- package/dist/views/InventoryPage.d.ts.map +1 -1
- package/dist/views/UsageAuditView.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React3, {
|
|
1
|
+
import React3, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, ConfirmDialog, Badge, DraftCommitBar, Switch, useSaveBar, DataTable, DashboardCanvas, Thumbnail, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, StockEntryForm, KpiCard, Card, CardHeader, CardTitle, CardContent, Skeleton } from '@fayz-ai/ui';
|
|
3
|
-
import { createPluginContext, PluginSettingsPanel, dedup, useModuleNavigation, createViewRouter, ModuleActionBar,
|
|
3
|
+
import { createPluginContext, PluginSettingsPanel, dedup, useTenantPluginSettings, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, usePermissionOptional, CrudListView, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, useAgentSurface, askAI, PermissionGate, QuickActionsButton, invalidateLimit } from '@fayz-ai/admin';
|
|
4
4
|
import { createSafeDataProvider, registerTranslations, countByTenant, useTranslation, getActiveUnitId, createPendingCommand, errorMessage, useDataChanged, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, ClipboardCheck, AlertTriangle, Plus, BookOpen, Layers, Clock, Coins, Building2, Undo2, PackageX } from 'lucide-react';
|
|
6
|
+
import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, ListFilter, CalendarRange, PackageCheck, ClipboardCheck, AlertTriangle, Plus, BookOpen, Layers, Clock, Coins, Building2, Undo2, PackageX } from 'lucide-react';
|
|
7
7
|
import { createStore as createStore$1 } from 'zustand/vanilla';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -2861,7 +2861,11 @@ function formatQuantity(value) {
|
|
|
2861
2861
|
function formatSigned(value) {
|
|
2862
2862
|
return value > 0 ? `+${formatQuantity(value)}` : formatQuantity(value);
|
|
2863
2863
|
}
|
|
2864
|
-
function formatUsedAt(iso, locale) {
|
|
2864
|
+
function formatUsedAt(iso, locale, dia) {
|
|
2865
|
+
if (dia && /^\d{4}-\d{2}-\d{2}$/.test(dia)) {
|
|
2866
|
+
const [a, m, d] = dia.split("-");
|
|
2867
|
+
return new Date(Number(a), Number(m) - 1, Number(d)).toLocaleDateString(locale, { day: "2-digit", month: "2-digit", year: "2-digit" });
|
|
2868
|
+
}
|
|
2865
2869
|
const date = new Date(iso);
|
|
2866
2870
|
if (Number.isNaN(date.getTime())) return "\u2014";
|
|
2867
2871
|
return date.toLocaleString(locale, {
|
|
@@ -2983,14 +2987,40 @@ function UsageAuditView() {
|
|
|
2983
2987
|
const loading = useInventoryStore((s) => s.usageAuditLoading);
|
|
2984
2988
|
const fetchUsageAudit = useInventoryStore((s) => s.fetchUsageAudit);
|
|
2985
2989
|
const correctUsageBatch = useInventoryStore((s) => s.correctUsageBatch);
|
|
2990
|
+
const approveUsageBatch = useInventoryStore((s) => s.approveUsageBatch);
|
|
2991
|
+
const patchUsageAuditEntries = useInventoryStore((s) => s.patchUsageAuditEntries);
|
|
2986
2992
|
const [search, setSearch] = useState("");
|
|
2987
|
-
const [
|
|
2993
|
+
const [queue, setQueue] = useState("pending");
|
|
2988
2994
|
const [drafts, setDrafts] = useState({});
|
|
2989
2995
|
const [confirming, setConfirming] = useState(false);
|
|
2990
2996
|
const [saving, setSaving] = useState(false);
|
|
2997
|
+
const [loadedOnce, setLoadedOnce] = useState(false);
|
|
2998
|
+
const [dias, setDias] = useState(30);
|
|
2999
|
+
const [erro, setErro] = useState(null);
|
|
3000
|
+
const desde = useMemo(() => {
|
|
3001
|
+
if (dias == null) return void 0;
|
|
3002
|
+
const d = /* @__PURE__ */ new Date();
|
|
3003
|
+
d.setDate(d.getDate() - dias);
|
|
3004
|
+
return d.toISOString();
|
|
3005
|
+
}, [dias]);
|
|
2991
3006
|
useEffect(() => {
|
|
2992
|
-
|
|
2993
|
-
|
|
3007
|
+
let active = true;
|
|
3008
|
+
setErro(null);
|
|
3009
|
+
fetchUsageAudit({
|
|
3010
|
+
search: search || void 0,
|
|
3011
|
+
divergentOnly: queue === "divergent",
|
|
3012
|
+
pendingOnly: queue === "pending",
|
|
3013
|
+
from: desde
|
|
3014
|
+
}).catch((err) => {
|
|
3015
|
+
if (active) setErro(err?.message ?? String(err));
|
|
3016
|
+
toast.error(t2("inventory.usageAudit.loadFailed"), { description: err?.message });
|
|
3017
|
+
}).finally(() => {
|
|
3018
|
+
if (active) setLoadedOnce(true);
|
|
3019
|
+
});
|
|
3020
|
+
return () => {
|
|
3021
|
+
active = false;
|
|
3022
|
+
};
|
|
3023
|
+
}, [search, queue, desde]);
|
|
2994
3024
|
const locale = currency.locale ?? "pt-BR";
|
|
2995
3025
|
const fieldRefs = useRef(/* @__PURE__ */ new Map());
|
|
2996
3026
|
const order = useRef([]);
|
|
@@ -3024,6 +3054,60 @@ function UsageAuditView() {
|
|
|
3024
3054
|
() => entries.filter((entry) => parseQuantity(drafts[keyOf(entry)]) === void 0).length,
|
|
3025
3055
|
[entries, drafts]
|
|
3026
3056
|
);
|
|
3057
|
+
const [conferidas, setConferidas] = useState({});
|
|
3058
|
+
const [aprovando, setAprovando] = useState(/* @__PURE__ */ new Set());
|
|
3059
|
+
const aprovarLinhas = useCallback(async (alvos, baixar) => {
|
|
3060
|
+
if (!alvos.length) return;
|
|
3061
|
+
const chaves = alvos.map(keyOf);
|
|
3062
|
+
setAprovando((prev) => /* @__PURE__ */ new Set([...prev, ...chaves]));
|
|
3063
|
+
try {
|
|
3064
|
+
const lines = alvos.map((e) => ({ itemId: e.itemId, productId: e.productId }));
|
|
3065
|
+
const r = await approveUsageBatch({ post: baixar, lines });
|
|
3066
|
+
const semSaldo = r.linesNoStock ?? 0;
|
|
3067
|
+
const passaram = baixar ? r.linesPosted : r.linesApproved;
|
|
3068
|
+
if (baixar && r.linesPosted === 0 && semSaldo > 0) {
|
|
3069
|
+
const f = r.missing?.[0];
|
|
3070
|
+
toast.warning(t2("inventory.usageAudit.noStock"), {
|
|
3071
|
+
description: f ? `${f.product} \u2014 precisa ${f.needed}, h\xE1 ${f.available}` : void 0
|
|
3072
|
+
});
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
toast.success(baixar ? t2("inventory.usageAudit.posted", { count: String(r.linesPosted) }) : t2("inventory.usageAudit.approved", { count: String(r.linesApproved) }), {
|
|
3076
|
+
// Um lote parcial precisa dizer quantas ficaram para trás, ou some a
|
|
3077
|
+
// diferença entre "aprovei 12" e "pedi 15".
|
|
3078
|
+
description: semSaldo > 0 ? t2("inventory.usageAudit.batchNoStock", { count: String(semSaldo) }) : void 0
|
|
3079
|
+
});
|
|
3080
|
+
if (semSaldo > 0 || passaram !== alvos.length) {
|
|
3081
|
+
await fetchUsageAudit({
|
|
3082
|
+
search: search || void 0,
|
|
3083
|
+
divergentOnly: queue === "divergent",
|
|
3084
|
+
pendingOnly: queue === "pending",
|
|
3085
|
+
from: desde
|
|
3086
|
+
});
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
patchUsageAuditEntries(lines, { isCorrected: true, ...baixar ? { stockPosted: true } : {} });
|
|
3090
|
+
setConferidas((prev) => {
|
|
3091
|
+
const next = { ...prev };
|
|
3092
|
+
for (const k of chaves) next[k] = baixar ? "posted" : "approved";
|
|
3093
|
+
return next;
|
|
3094
|
+
});
|
|
3095
|
+
} catch (err) {
|
|
3096
|
+
toast.error(t2("inventory.usageAudit.approveFailed"), { description: err?.message });
|
|
3097
|
+
} finally {
|
|
3098
|
+
setAprovando((prev) => {
|
|
3099
|
+
const next = new Set(prev);
|
|
3100
|
+
for (const k of chaves) next.delete(k);
|
|
3101
|
+
return next;
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
}, [approveUsageBatch, patchUsageAuditEntries, fetchUsageAudit, search, queue, desde, t2]);
|
|
3105
|
+
const conferidasRef = useRef(conferidas);
|
|
3106
|
+
conferidasRef.current = conferidas;
|
|
3107
|
+
const aprovandoRef = useRef(aprovando);
|
|
3108
|
+
aprovandoRef.current = aprovando;
|
|
3109
|
+
const aprovarLinhasRef = useRef(aprovarLinhas);
|
|
3110
|
+
aprovarLinhasRef.current = aprovarLinhas;
|
|
3027
3111
|
async function apply(reason) {
|
|
3028
3112
|
setSaving(true);
|
|
3029
3113
|
try {
|
|
@@ -3048,10 +3132,17 @@ function UsageAuditView() {
|
|
|
3048
3132
|
header: t2("inventory.usageAudit.columnProduct"),
|
|
3049
3133
|
cell: ({ row }) => {
|
|
3050
3134
|
const entry = row.original;
|
|
3051
|
-
return
|
|
3052
|
-
/*
|
|
3053
|
-
|
|
3054
|
-
|
|
3135
|
+
return (
|
|
3136
|
+
/* NOME LONGO NÃO EMPURRA A TABELA. "Ao Redor da Boca e Código de
|
|
3137
|
+
Barras - Ultraformer" alargava a coluna e espremia quantidade,
|
|
3138
|
+
profissional e as ações para fora da tela. Aqui o nome tem teto e
|
|
3139
|
+
corta com reticências — o inteiro fica no `title`, a um passar de
|
|
3140
|
+
mouse, e a linha continua legível no celular. */
|
|
3141
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-[10rem] max-w-[18rem]", children: [
|
|
3142
|
+
/* @__PURE__ */ jsx("div", { className: "truncate font-medium", title: entry.productName ?? void 0, children: entry.productName ?? t2("inventory.usageAudit.unnamedProduct") }),
|
|
3143
|
+
entry.itemName && entry.itemName !== entry.productName && /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", title: entry.itemName, children: entry.itemName })
|
|
3144
|
+
] })
|
|
3145
|
+
);
|
|
3055
3146
|
}
|
|
3056
3147
|
},
|
|
3057
3148
|
{
|
|
@@ -3072,7 +3163,14 @@ function UsageAuditView() {
|
|
|
3072
3163
|
formatQuantity(entry.standardQuantity),
|
|
3073
3164
|
unit
|
|
3074
3165
|
] }),
|
|
3075
|
-
entry.isCorrected && /* @__PURE__ */ jsx(
|
|
3166
|
+
(entry.isCorrected || conferidasRef.current[keyOf(entry)]) && /* @__PURE__ */ jsx(
|
|
3167
|
+
Badge,
|
|
3168
|
+
{
|
|
3169
|
+
variant: conferidasRef.current[keyOf(entry)] ? "success" : "outline",
|
|
3170
|
+
className: "ml-2 text-[10px]",
|
|
3171
|
+
children: conferidasRef.current[keyOf(entry)] === "posted" ? t2("inventory.usageAudit.badgePosted") : conferidasRef.current[keyOf(entry)] === "approved" ? t2("inventory.usageAudit.badgeApproved") : t2("inventory.usageAudit.corrected")
|
|
3172
|
+
}
|
|
3173
|
+
)
|
|
3076
3174
|
] });
|
|
3077
3175
|
}
|
|
3078
3176
|
},
|
|
@@ -3090,14 +3188,14 @@ function UsageAuditView() {
|
|
|
3090
3188
|
{
|
|
3091
3189
|
id: "usedAt",
|
|
3092
3190
|
header: t2("inventory.usageAudit.columnWhen"),
|
|
3093
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: formatUsedAt(row.original.usedAt, locale) })
|
|
3191
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: formatUsedAt(row.original.usedAt, locale, row.original.movementDate) })
|
|
3094
3192
|
},
|
|
3095
3193
|
{
|
|
3096
3194
|
id: "performedBy",
|
|
3097
3195
|
header: t2("inventory.usageAudit.columnPerformedBy"),
|
|
3098
3196
|
cell: ({ row }) => {
|
|
3099
3197
|
const entry = row.original;
|
|
3100
|
-
return /* @__PURE__ */ jsx("span", { className: "text-xs
|
|
3198
|
+
return /* @__PURE__ */ jsx("span", { className: "block max-w-[10rem] truncate text-xs", title: entry.performedByName ?? void 0, children: entry.performedByName ?? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) });
|
|
3101
3199
|
}
|
|
3102
3200
|
},
|
|
3103
3201
|
{
|
|
@@ -3105,7 +3203,7 @@ function UsageAuditView() {
|
|
|
3105
3203
|
header: t2("inventory.usageAudit.columnCustomer"),
|
|
3106
3204
|
cell: ({ row }) => {
|
|
3107
3205
|
const entry = row.original;
|
|
3108
|
-
return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground
|
|
3206
|
+
return /* @__PURE__ */ jsx("span", { className: "block max-w-[12rem] truncate text-xs text-muted-foreground", title: entry.customerName ?? void 0, children: entry.customerName ?? "\u2014" });
|
|
3109
3207
|
}
|
|
3110
3208
|
},
|
|
3111
3209
|
{
|
|
@@ -3132,35 +3230,119 @@ function UsageAuditView() {
|
|
|
3132
3230
|
}
|
|
3133
3231
|
);
|
|
3134
3232
|
}
|
|
3233
|
+
},
|
|
3234
|
+
{
|
|
3235
|
+
// AS DUAS AÇÕES DO V1, na linha, com os verbos dele.
|
|
3236
|
+
//
|
|
3237
|
+
// "Baixar no estoque" aceita E desconta; "Aprovar" aceita e não mexe no
|
|
3238
|
+
// saldo — é o consumo que não sai da prateleira da casa (produto do
|
|
3239
|
+
// próprio cliente, amostra, item já baixado por fora). Separá-las é o
|
|
3240
|
+
// que impede que conferir vire baixa automática.
|
|
3241
|
+
id: "acoes",
|
|
3242
|
+
header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: t2("inventory.usageAudit.columnActions") }),
|
|
3243
|
+
cell: ({ row }) => {
|
|
3244
|
+
const entry = row.original;
|
|
3245
|
+
const key = keyOf(entry);
|
|
3246
|
+
const feito = !!conferidasRef.current[key];
|
|
3247
|
+
const ocupado = aprovandoRef.current.has(key);
|
|
3248
|
+
if (feito) return /* @__PURE__ */ jsx("span", { className: "block text-right text-xs text-muted-foreground", children: "\u2014" });
|
|
3249
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-1", children: [
|
|
3250
|
+
/* @__PURE__ */ jsx(
|
|
3251
|
+
"button",
|
|
3252
|
+
{
|
|
3253
|
+
type: "button",
|
|
3254
|
+
disabled: ocupado,
|
|
3255
|
+
onClick: () => void aprovarLinhasRef.current([entry], true),
|
|
3256
|
+
title: t2("inventory.usageAudit.postHint"),
|
|
3257
|
+
className: "rounded-button bg-foreground px-2 py-1 text-[11px] font-semibold text-background disabled:opacity-40",
|
|
3258
|
+
children: t2("inventory.usageAudit.post")
|
|
3259
|
+
}
|
|
3260
|
+
),
|
|
3261
|
+
/* @__PURE__ */ jsx(
|
|
3262
|
+
"button",
|
|
3263
|
+
{
|
|
3264
|
+
type: "button",
|
|
3265
|
+
disabled: ocupado,
|
|
3266
|
+
onClick: () => void aprovarLinhasRef.current([entry], false),
|
|
3267
|
+
title: t2("inventory.usageAudit.approveHint"),
|
|
3268
|
+
className: "rounded-button border border-input px-2 py-1 text-[11px] font-medium hover:bg-muted disabled:opacity-40",
|
|
3269
|
+
children: t2("inventory.usageAudit.approve")
|
|
3270
|
+
}
|
|
3271
|
+
)
|
|
3272
|
+
] });
|
|
3273
|
+
}
|
|
3135
3274
|
}
|
|
3275
|
+
// As colunas NÃO dependem de `conferidas` nem de `aprovando`: refazê-las
|
|
3276
|
+
// remonta a célula do campo aberto e leva o foco junto — o mesmo motivo que
|
|
3277
|
+
// já obrigava `draftsRef`. As duas leituras voláteis vão por ref, e quem
|
|
3278
|
+
// repinta a linha é o `usageAudit` do store, que a aprovação acabou de
|
|
3279
|
+
// mudar.
|
|
3136
3280
|
], [currency, locale, t2, setDraft, focusNext, registerRef]);
|
|
3137
3281
|
const costDelta = summary.varianceValue;
|
|
3138
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-
|
|
3139
|
-
/* @__PURE__ */
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
}
|
|
3145
|
-
),
|
|
3282
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
3283
|
+
erro && /* @__PURE__ */ jsxs("p", { className: "rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive", children: [
|
|
3284
|
+
t2("inventory.usageAudit.loadFailed"),
|
|
3285
|
+
" \u2014 ",
|
|
3286
|
+
erro
|
|
3287
|
+
] }),
|
|
3146
3288
|
/* @__PURE__ */ jsx(
|
|
3147
3289
|
ListView,
|
|
3148
3290
|
{
|
|
3149
3291
|
columns,
|
|
3150
3292
|
data: entries,
|
|
3151
|
-
loading,
|
|
3293
|
+
loading: loading || !loadedOnce,
|
|
3152
3294
|
total,
|
|
3153
3295
|
getRowId: (row) => keyOf(row),
|
|
3296
|
+
bulkActions: [
|
|
3297
|
+
{
|
|
3298
|
+
id: "post",
|
|
3299
|
+
label: t2("inventory.usageAudit.post"),
|
|
3300
|
+
icon: PackageCheck,
|
|
3301
|
+
onSelect: (rows) => aprovarLinhas(rows.filter((r) => !conferidas[keyOf(r)]), true)
|
|
3302
|
+
},
|
|
3303
|
+
{
|
|
3304
|
+
id: "approve",
|
|
3305
|
+
label: t2("inventory.usageAudit.approve"),
|
|
3306
|
+
icon: Check,
|
|
3307
|
+
onSelect: (rows) => aprovarLinhas(rows.filter((r) => !conferidas[keyOf(r)]), false)
|
|
3308
|
+
}
|
|
3309
|
+
],
|
|
3154
3310
|
searchPlaceholder: t2("inventory.usageAudit.searchPlaceholder"),
|
|
3155
3311
|
search,
|
|
3156
3312
|
onSearchChange: setSearch,
|
|
3157
3313
|
searchDebounce: 300,
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3314
|
+
filterDefs: [
|
|
3315
|
+
{
|
|
3316
|
+
id: "queue",
|
|
3317
|
+
label: t2("inventory.usageAudit.filterLabel"),
|
|
3318
|
+
icon: ListFilter,
|
|
3319
|
+
allLabel: t2("inventory.usageAudit.filterAll"),
|
|
3320
|
+
options: [
|
|
3321
|
+
{ value: "pending", label: t2("inventory.usageAudit.filterPending") },
|
|
3322
|
+
{ value: "divergent", label: t2("inventory.usageAudit.filterDivergent") }
|
|
3323
|
+
],
|
|
3324
|
+
value: queue === "all" ? void 0 : queue,
|
|
3325
|
+
onChange: (v) => setQueue(v ?? "all")
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
id: "period",
|
|
3329
|
+
label: t2("inventory.usageAudit.periodLabel"),
|
|
3330
|
+
icon: CalendarRange,
|
|
3331
|
+
allLabel: t2("inventory.usageAudit.periodAll"),
|
|
3332
|
+
options: [
|
|
3333
|
+
{ value: "30", label: t2("inventory.usageAudit.period30") },
|
|
3334
|
+
{ value: "90", label: t2("inventory.usageAudit.period90") }
|
|
3335
|
+
],
|
|
3336
|
+
value: dias == null ? void 0 : String(dias),
|
|
3337
|
+
onChange: (v) => setDias(v ? Number(v) : null)
|
|
3338
|
+
}
|
|
3339
|
+
],
|
|
3340
|
+
filtersLabel: t2("inventory.usageAudit.filtersLabel"),
|
|
3341
|
+
onClearFilters: () => {
|
|
3342
|
+
setQueue("all");
|
|
3343
|
+
setDias(null);
|
|
3344
|
+
},
|
|
3345
|
+
emptyMessage: erro ? t2("inventory.usageAudit.loadFailed") : queue === "divergent" ? t2("inventory.usageAudit.emptyDivergent") : queue === "pending" ? t2("inventory.usageAudit.emptyPending") : t2("inventory.usageAudit.empty")
|
|
3164
3346
|
}
|
|
3165
3347
|
),
|
|
3166
3348
|
/* @__PURE__ */ jsx(
|
|
@@ -3190,6 +3372,9 @@ function UsageAuditView() {
|
|
|
3190
3372
|
)
|
|
3191
3373
|
] });
|
|
3192
3374
|
}
|
|
3375
|
+
var INVENTORY_MODULE_DEFAULTS = {
|
|
3376
|
+
recipes: true
|
|
3377
|
+
};
|
|
3193
3378
|
var INVENTORY_DEFAULTS = {
|
|
3194
3379
|
lowStockAlerts: true,
|
|
3195
3380
|
requireReason: true,
|
|
@@ -3199,10 +3384,20 @@ var INVENTORY_DEFAULTS = {
|
|
|
3199
3384
|
lowStockEmail: false,
|
|
3200
3385
|
expiryWarnings: true
|
|
3201
3386
|
};
|
|
3202
|
-
function InventoryGeneralSettings() {
|
|
3387
|
+
function InventoryGeneralSettings({ config }) {
|
|
3203
3388
|
const t2 = useTranslation();
|
|
3204
3389
|
const s = useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
|
|
3390
|
+
const mods = useTenantPluginSettings("inventory", INVENTORY_MODULE_DEFAULTS);
|
|
3205
3391
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
3392
|
+
config?.modules.recipes && /* @__PURE__ */ jsx(SettingsGroup, { title: t2("inventory.settings.modules"), description: t2("inventory.settings.modulesDesc"), children: /* @__PURE__ */ jsx(
|
|
3393
|
+
ToggleRow,
|
|
3394
|
+
{
|
|
3395
|
+
label: t2("inventory.settings.recipes"),
|
|
3396
|
+
description: t2("inventory.settings.recipesDesc"),
|
|
3397
|
+
checked: mods.get("recipes"),
|
|
3398
|
+
onChange: (v) => mods.set("recipes", v)
|
|
3399
|
+
}
|
|
3400
|
+
) }),
|
|
3206
3401
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
3207
3402
|
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
|
|
3208
3403
|
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
|
|
@@ -3294,36 +3489,53 @@ function buildNav(config, view, navigate, t2) {
|
|
|
3294
3489
|
{ id: "stock-history", label: t2("inventory.nav.history"), defaultChild: true, active: view === "stock-history", onClick: () => navigate("stock-history") }
|
|
3295
3490
|
]
|
|
3296
3491
|
},
|
|
3297
|
-
//
|
|
3298
|
-
//
|
|
3299
|
-
//
|
|
3300
|
-
//
|
|
3492
|
+
// Contagem e Auditoria de Uso são ABAS, não uma rubrica com pílula dentro.
|
|
3493
|
+
//
|
|
3494
|
+
// Conferir o que o sistema diz contra o que existe na prateleira são duas
|
|
3495
|
+
// TELAS, cada uma com a sua lista e o seu fechamento — e agrupá-las sob
|
|
3496
|
+
// "Inventário" punha o segundo nível num controle segmentado à direita da
|
|
3497
|
+
// faixa, que é onde ninguém procura uma tela. A faixa é o lugar de uma
|
|
3498
|
+
// tela; a pílula é o lugar de um recorte da mesma tela.
|
|
3499
|
+
//
|
|
3500
|
+
// "Nova contagem" saiu junto: era uma AÇÃO dentro de navegação, e a lista
|
|
3501
|
+
// de contagens já tem o "+" no cabeçalho dela.
|
|
3301
3502
|
{
|
|
3302
|
-
id: "
|
|
3303
|
-
label: t2("inventory.nav.
|
|
3503
|
+
id: "stock-counts",
|
|
3504
|
+
label: t2("inventory.nav.counts"),
|
|
3304
3505
|
icon: "ClipboardCheck",
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3506
|
+
active: view === "stock-counts" || view.startsWith("stock-counts"),
|
|
3507
|
+
onClick: () => navigate("stock-counts")
|
|
3508
|
+
},
|
|
3509
|
+
{
|
|
3510
|
+
id: "stock-usage-audit",
|
|
3511
|
+
label: t2("inventory.nav.usageAudit"),
|
|
3512
|
+
icon: "ScanSearch",
|
|
3513
|
+
active: view === "stock-usage-audit",
|
|
3514
|
+
onClick: () => navigate("stock-usage-audit")
|
|
3310
3515
|
}
|
|
3311
3516
|
];
|
|
3312
3517
|
if (config.modules.recipes) {
|
|
3518
|
+
const rotulo = (chave, escolhido, padrao) => escolhido && escolhido !== padrao ? escolhido : t2(chave);
|
|
3313
3519
|
items.push({
|
|
3314
3520
|
id: "recipes",
|
|
3315
|
-
label: config.labels.recipes,
|
|
3521
|
+
label: rotulo("inventory.nav.recipes", config.labels.recipes, "Recipes"),
|
|
3316
3522
|
icon: "BookOpen",
|
|
3317
3523
|
children: [
|
|
3318
|
-
{ id: "recipes-list", label: config.labels.recipesList, active: view === "recipes-list" || view.startsWith("recipes-detail:"), onClick: () => navigate("recipes-list") },
|
|
3319
|
-
{ id: "recipes-new", kind: "action", label: config.labels.recipesNew, active: view === "recipes-new", onClick: () => navigate("recipes-new") }
|
|
3524
|
+
{ id: "recipes-list", label: rotulo("inventory.nav.list", config.labels.recipesList, "List"), active: view === "recipes-list" || view.startsWith("recipes-detail:"), onClick: () => navigate("recipes-list") },
|
|
3525
|
+
{ id: "recipes-new", kind: "action", label: rotulo("inventory.nav.new", config.labels.recipesNew, "New"), active: view === "recipes-new", onClick: () => navigate("recipes-new") }
|
|
3320
3526
|
]
|
|
3321
3527
|
});
|
|
3322
3528
|
}
|
|
3323
3529
|
return items;
|
|
3324
3530
|
}
|
|
3325
|
-
function InventoryPage({ config, provider, store, registries }) {
|
|
3531
|
+
function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
3326
3532
|
const t2 = useTranslation();
|
|
3533
|
+
const houseModules = useTenantPluginSettings("inventory", INVENTORY_MODULE_DEFAULTS);
|
|
3534
|
+
const recipesOn = houseModules.get("recipes");
|
|
3535
|
+
const config = useMemo(
|
|
3536
|
+
() => appConfig.modules.recipes && recipesOn ? appConfig : { ...appConfig, modules: { ...appConfig.modules, recipes: false } },
|
|
3537
|
+
[appConfig, recipesOn]
|
|
3538
|
+
);
|
|
3327
3539
|
const { view, direction, navigate } = useModuleNavigation("/inventory", {
|
|
3328
3540
|
dashboard: 0,
|
|
3329
3541
|
"products-list": 0,
|
|
@@ -3399,7 +3611,7 @@ function InventoryPage({ config, provider, store, registries }) {
|
|
|
3399
3611
|
PluginSettingsPanel,
|
|
3400
3612
|
{
|
|
3401
3613
|
title: t2("inventory.settingsPage.title"),
|
|
3402
|
-
generalSettings: /* @__PURE__ */ jsx(InventoryGeneralSettings, {}),
|
|
3614
|
+
generalSettings: /* @__PURE__ */ jsx(InventoryGeneralSettings, { config: appConfig }),
|
|
3403
3615
|
registries,
|
|
3404
3616
|
hostPluginId: "inventory",
|
|
3405
3617
|
routeBase: "/inventory/settings"
|
|
@@ -4378,7 +4590,8 @@ function camelToSnake(obj) {
|
|
|
4378
4590
|
}
|
|
4379
4591
|
var PRODUCT_SELECT = `*, ${T.productDetails}(*)`;
|
|
4380
4592
|
var PRODUCTS_READ = "v_inventory_products";
|
|
4381
|
-
var
|
|
4593
|
+
var soMercadoria = (qb) => qb.neq("kind", "service");
|
|
4594
|
+
var USAGE_AUDIT_READ = "v_inventory_consumption_audit";
|
|
4382
4595
|
async function defaultLocationId(pub, tenantId) {
|
|
4383
4596
|
if (!tenantId) return void 0;
|
|
4384
4597
|
const { data } = await pub.from("plg_inventory_stock_locations").select("id").eq("tenant_id", tenantId).eq("is_active", true).order("created_at").limit(1);
|
|
@@ -4658,7 +4871,7 @@ function createSupabaseInventoryProvider() {
|
|
|
4658
4871
|
async getProducts(query) {
|
|
4659
4872
|
const { core} = getClients();
|
|
4660
4873
|
const select = query.categoryId ? `*, ${T.productDetails}!inner(*)` : PRODUCT_SELECT;
|
|
4661
|
-
let qb = core.from(PRODUCTS_READ).select(select, { count: "exact" });
|
|
4874
|
+
let qb = soMercadoria(core.from(PRODUCTS_READ).select(select, { count: "exact" }));
|
|
4662
4875
|
if (query.search) qb = qb.ilike("name", `%${query.search}%`);
|
|
4663
4876
|
if (query.productType) qb = qb.eq("metadata->>productType", query.productType);
|
|
4664
4877
|
if (query.categoryId) qb = qb.eq(`${T.productDetails}.category_id`, query.categoryId);
|
|
@@ -5243,8 +5456,12 @@ function createSupabaseInventoryProvider() {
|
|
|
5243
5456
|
// --- Usage audit ---
|
|
5244
5457
|
async getUsageAudit(query) {
|
|
5245
5458
|
const { pub } = getClients();
|
|
5246
|
-
let qb = pub.from(USAGE_AUDIT_READ).select("*", { count: "
|
|
5459
|
+
let qb = pub.from(USAGE_AUDIT_READ).select("*", { count: "planned" });
|
|
5247
5460
|
if (query.divergentOnly) qb = qb.not("variance", "is", null).neq("variance", 0);
|
|
5461
|
+
if (query.pendingOnly) qb = qb.eq("is_corrected", false);
|
|
5462
|
+
const unidade = getActiveUnitId();
|
|
5463
|
+
if (unidade) qb = qb.eq("unit_id", unidade);
|
|
5464
|
+
if (query.from) qb = qb.gte("used_at", query.from);
|
|
5248
5465
|
if (query.search) {
|
|
5249
5466
|
const like = `%${query.search.replace(/[(),]/g, " ").trim()}%`;
|
|
5250
5467
|
qb = qb.or(
|
|
@@ -5256,6 +5473,12 @@ function createSupabaseInventoryProvider() {
|
|
|
5256
5473
|
qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("used_at", { ascending: false });
|
|
5257
5474
|
const { data, count, error } = await qb;
|
|
5258
5475
|
if (error) throw new Error(error.message);
|
|
5476
|
+
const ids = [...new Set((data ?? []).flatMap((r) => [r.performed_by_id, r.customer_id]).filter(Boolean))];
|
|
5477
|
+
const nomes = /* @__PURE__ */ new Map();
|
|
5478
|
+
if (ids.length) {
|
|
5479
|
+
const { data: pessoas } = await pub.from("people").select("id, name").in("id", ids);
|
|
5480
|
+
for (const p of pessoas ?? []) nomes.set(p.id, p.name);
|
|
5481
|
+
}
|
|
5259
5482
|
const entries = (data ?? []).map((r) => ({
|
|
5260
5483
|
itemId: r.item_id,
|
|
5261
5484
|
productId: r.product_id,
|
|
@@ -5280,12 +5503,17 @@ function createSupabaseInventoryProvider() {
|
|
|
5280
5503
|
itemName: r.item_name ?? void 0,
|
|
5281
5504
|
orderReference: r.order_reference ?? void 0,
|
|
5282
5505
|
performedById: r.performed_by_id ?? void 0,
|
|
5283
|
-
performedByName: r.performed_by_name ?? void 0,
|
|
5506
|
+
performedByName: r.performed_by_name ?? nomes.get(r.performed_by_id) ?? void 0,
|
|
5284
5507
|
customerId: r.customer_id ?? void 0,
|
|
5285
|
-
customerName: r.customer_name ?? void 0,
|
|
5508
|
+
customerName: r.customer_name ?? nomes.get(r.customer_id) ?? void 0,
|
|
5286
5509
|
correctedAt: r.corrected_at ?? void 0,
|
|
5287
5510
|
correctionReason: r.correction_reason ?? void 0,
|
|
5288
|
-
isCorrected: Boolean(r.is_corrected)
|
|
5511
|
+
isCorrected: Boolean(r.is_corrected),
|
|
5512
|
+
// `ledger` quando o pedido baixou estoque, `clinical` quando o
|
|
5513
|
+
// atendimento consumiu e nada saiu do razão. Sem isto a tela não
|
|
5514
|
+
// consegue dizer ao operador que aquela linha nunca baixou.
|
|
5515
|
+
source: r.source ?? void 0,
|
|
5516
|
+
stockPosted: r.stock_posted == null ? void 0 : Boolean(r.stock_posted)
|
|
5289
5517
|
}));
|
|
5290
5518
|
return { data: entries, total: count ?? 0 };
|
|
5291
5519
|
},
|
|
@@ -5299,6 +5527,27 @@ function createSupabaseInventoryProvider() {
|
|
|
5299
5527
|
});
|
|
5300
5528
|
if (error) throw new Error(error.message);
|
|
5301
5529
|
},
|
|
5530
|
+
async approveUsageBatch(input) {
|
|
5531
|
+
const { pub } = getClients();
|
|
5532
|
+
const { data, error } = await pub.rpc("inventory_approve_usage_batch", {
|
|
5533
|
+
p_lines: input.lines.map((l) => ({ item_id: l.itemId, product_id: l.productId })),
|
|
5534
|
+
p_post: input.post,
|
|
5535
|
+
p_reason: input.reason ?? null
|
|
5536
|
+
});
|
|
5537
|
+
if (error) throw new Error(error.message);
|
|
5538
|
+
const r = data ?? {};
|
|
5539
|
+
return {
|
|
5540
|
+
linesApproved: Number(r.lines_approved ?? 0),
|
|
5541
|
+
linesPosted: Number(r.lines_posted ?? 0),
|
|
5542
|
+
linesSkipped: Number(r.lines_skipped ?? 0),
|
|
5543
|
+
linesNoStock: Number(r.lines_no_stock ?? 0),
|
|
5544
|
+
missing: (r.missing ?? []).map((m) => ({
|
|
5545
|
+
product: m.product,
|
|
5546
|
+
needed: Number(m.needed ?? 0),
|
|
5547
|
+
available: Number(m.available ?? 0)
|
|
5548
|
+
}))
|
|
5549
|
+
};
|
|
5550
|
+
},
|
|
5302
5551
|
async correctUsageBatch(input) {
|
|
5303
5552
|
const { pub } = getClients();
|
|
5304
5553
|
const { data, error } = await pub.rpc("inventory_correct_usage_batch", {
|
|
@@ -5322,7 +5571,9 @@ function createSupabaseInventoryProvider() {
|
|
|
5322
5571
|
// --- Summary ---
|
|
5323
5572
|
async getSummary() {
|
|
5324
5573
|
const { core, pub } = getClients();
|
|
5325
|
-
const { data: products2 } = await
|
|
5574
|
+
const { data: products2 } = await soMercadoria(
|
|
5575
|
+
core.from(PRODUCTS_READ).select("stock, min_stock, price, is_active").eq("is_active", true)
|
|
5576
|
+
);
|
|
5326
5577
|
const items = products2 ?? [];
|
|
5327
5578
|
const lowStock = items.filter((p) => p.stock > 0 && p.stock <= (p.min_stock ?? 0));
|
|
5328
5579
|
const outOfStock = items.filter((p) => p.stock <= 0);
|
|
@@ -5347,7 +5598,7 @@ function createSupabaseInventoryProvider() {
|
|
|
5347
5598
|
},
|
|
5348
5599
|
async getRestockList(limit = 50) {
|
|
5349
5600
|
const { core, pub } = getClients();
|
|
5350
|
-
const { data } = await core.from(PRODUCTS_READ).select("id, name, category_id, stock, min_stock, unit").eq("is_active", true);
|
|
5601
|
+
const { data } = await soMercadoria(core.from(PRODUCTS_READ).select("id, name, category_id, stock, min_stock, unit").eq("is_active", true));
|
|
5351
5602
|
const rows = data ?? [];
|
|
5352
5603
|
const needs = rows.map((r) => ({
|
|
5353
5604
|
productId: r.id,
|
|
@@ -5374,7 +5625,9 @@ function createSupabaseInventoryProvider() {
|
|
|
5374
5625
|
},
|
|
5375
5626
|
async getCategoryDistribution() {
|
|
5376
5627
|
const { core, pub } = getClients();
|
|
5377
|
-
const { data } = await
|
|
5628
|
+
const { data } = await soMercadoria(
|
|
5629
|
+
core.from(PRODUCTS_READ).select("category_id").eq("is_active", true)
|
|
5630
|
+
);
|
|
5378
5631
|
const rows = data ?? [];
|
|
5379
5632
|
const tally = /* @__PURE__ */ new Map();
|
|
5380
5633
|
for (const r of rows) tally.set(r.category_id ?? null, (tally.get(r.category_id ?? null) ?? 0) + 1);
|
|
@@ -5418,57 +5671,59 @@ function createInventoryStore(provider) {
|
|
|
5418
5671
|
categoryMix: null,
|
|
5419
5672
|
async fetchRestock() {
|
|
5420
5673
|
if (!provider.getRestockList) return;
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
}
|
|
5427
|
-
});
|
|
5674
|
+
try {
|
|
5675
|
+
set({ restock: await dedup("inv:restock", () => provider.getRestockList()) });
|
|
5676
|
+
} catch {
|
|
5677
|
+
set({ restock: [] });
|
|
5678
|
+
}
|
|
5428
5679
|
},
|
|
5429
5680
|
async fetchCategoryMix() {
|
|
5430
5681
|
if (!provider.getCategoryDistribution) return;
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
}
|
|
5437
|
-
});
|
|
5682
|
+
try {
|
|
5683
|
+
set({ categoryMix: await dedup("inv:category-mix", () => provider.getCategoryDistribution()) });
|
|
5684
|
+
} catch {
|
|
5685
|
+
set({ categoryMix: [] });
|
|
5686
|
+
}
|
|
5438
5687
|
},
|
|
5439
5688
|
async fetchSummary() {
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5689
|
+
set({ summaryLoading: true });
|
|
5690
|
+
try {
|
|
5691
|
+
set({ summary: await dedup("inv:summary", () => provider.getSummary()), summaryLoading: false });
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
set({ summaryLoading: false });
|
|
5694
|
+
throw err;
|
|
5695
|
+
}
|
|
5445
5696
|
},
|
|
5446
5697
|
async fetchProducts(query) {
|
|
5447
5698
|
const key = "inv:products:" + JSON.stringify(query);
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
const result = await provider.getProducts(query);
|
|
5699
|
+
set({ productsLoading: true, productQuery: query });
|
|
5700
|
+
try {
|
|
5701
|
+
const result = await dedup(key, () => provider.getProducts(query));
|
|
5451
5702
|
set({ products: result.data, productsTotal: result.total, productsLoading: false });
|
|
5452
|
-
})
|
|
5703
|
+
} catch (err) {
|
|
5704
|
+
set({ productsLoading: false });
|
|
5705
|
+
throw err;
|
|
5706
|
+
}
|
|
5453
5707
|
},
|
|
5454
5708
|
async fetchMovements(query) {
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
const result = await provider.getMovements(query);
|
|
5709
|
+
set({ movementsLoading: true });
|
|
5710
|
+
try {
|
|
5711
|
+
const result = await dedup("inv:movements:" + JSON.stringify(query), () => provider.getMovements(query));
|
|
5458
5712
|
set({ movements: result.data, movementsTotal: result.total, movementsLoading: false });
|
|
5459
|
-
})
|
|
5713
|
+
} catch (err) {
|
|
5714
|
+
set({ movementsLoading: false });
|
|
5715
|
+
throw err;
|
|
5716
|
+
}
|
|
5460
5717
|
},
|
|
5461
5718
|
async fetchUsageAudit(query) {
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
}
|
|
5471
|
-
});
|
|
5719
|
+
set({ usageAuditLoading: true, usageAuditQuery: query });
|
|
5720
|
+
try {
|
|
5721
|
+
const result = await dedup("inv:usage-audit:" + JSON.stringify(query), () => provider.getUsageAudit(query));
|
|
5722
|
+
set({ usageAudit: result.data, usageAuditTotal: result.total, usageAuditLoading: false });
|
|
5723
|
+
} catch (err) {
|
|
5724
|
+
set({ usageAuditLoading: false });
|
|
5725
|
+
throw err;
|
|
5726
|
+
}
|
|
5472
5727
|
},
|
|
5473
5728
|
async correctUsage(input) {
|
|
5474
5729
|
await provider.correctUsage(input);
|
|
@@ -5478,6 +5733,28 @@ function createInventoryStore(provider) {
|
|
|
5478
5733
|
]);
|
|
5479
5734
|
set({ usageAudit: result.data, usageAuditTotal: result.total, summary });
|
|
5480
5735
|
},
|
|
5736
|
+
async approveUsageBatch(input) {
|
|
5737
|
+
if (!provider.approveUsageBatch) throw new Error("inventory: provider has no approveUsageBatch");
|
|
5738
|
+
return provider.approveUsageBatch(input);
|
|
5739
|
+
},
|
|
5740
|
+
// CONFERIR NÃO É RECARREGAR.
|
|
5741
|
+
//
|
|
5742
|
+
// Aprovar uma linha recarregava a fila inteira — e a fila do Espaço Facial
|
|
5743
|
+
// custa 0,65 s no banco com a janela curta, mais o tempo de montar 50
|
|
5744
|
+
// linhas. Quem confere desce a lista aprovando uma a uma: cada clique
|
|
5745
|
+
// jogava a tabela fora, refazia a rolagem e tirava do lugar a linha
|
|
5746
|
+
// seguinte, que é justamente onde a pessoa ia clicar.
|
|
5747
|
+
//
|
|
5748
|
+
// O servidor já disse o que aconteceu; a tela só precisa dizer o mesmo.
|
|
5749
|
+
// Nenhum `set` aqui inventa estado: `isCorrected` e `stockPosted` são os
|
|
5750
|
+
// dois campos que a aprovação de fato escreveu.
|
|
5751
|
+
patchUsageAuditEntries(lines, patch) {
|
|
5752
|
+
if (!lines.length) return;
|
|
5753
|
+
const alvo = new Set(lines.map((l) => `${l.itemId}:${l.productId}`));
|
|
5754
|
+
set((state) => ({
|
|
5755
|
+
usageAudit: state.usageAudit.map((entry) => alvo.has(`${entry.itemId}:${entry.productId}`) ? { ...entry, ...patch } : entry)
|
|
5756
|
+
}));
|
|
5757
|
+
},
|
|
5481
5758
|
async correctUsageBatch(input) {
|
|
5482
5759
|
const outcome = await provider.correctUsageBatch(input);
|
|
5483
5760
|
const [result, summary] = await Promise.all([
|
|
@@ -5488,11 +5765,13 @@ function createInventoryStore(provider) {
|
|
|
5488
5765
|
return outcome;
|
|
5489
5766
|
},
|
|
5490
5767
|
async fetchLocations() {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5768
|
+
set({ locationsLoading: true });
|
|
5769
|
+
try {
|
|
5770
|
+
set({ locations: await dedup("inv:locations", () => provider.getLocations()), locationsLoading: false });
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
set({ locationsLoading: false });
|
|
5773
|
+
throw err;
|
|
5774
|
+
}
|
|
5496
5775
|
},
|
|
5497
5776
|
async fetchRecipes() {
|
|
5498
5777
|
set({ recipesLoading: true });
|
|
@@ -6087,6 +6366,10 @@ var en = {
|
|
|
6087
6366
|
"inventory.quickActions.stockExitDesc": "Record goods used or sold",
|
|
6088
6367
|
"inventory.settingsPage.title": "Inventory Settings",
|
|
6089
6368
|
"inventory.settingsPage.subtitle": "Manage units, categories, and stock locations",
|
|
6369
|
+
"inventory.settings.modules": "Modules",
|
|
6370
|
+
"inventory.settings.modulesDesc": "What this house shows inside Inventory",
|
|
6371
|
+
"inventory.settings.recipes": "Recipes",
|
|
6372
|
+
"inventory.settings.recipesDesc": "The bill of materials that says what each product consumes",
|
|
6090
6373
|
"inventory.settings.stockManagement": "Stock Management",
|
|
6091
6374
|
"inventory.settings.stockManagementDesc": "How stock levels are tracked",
|
|
6092
6375
|
"inventory.settings.lowStockAlerts": "Low stock alerts",
|
|
@@ -6352,6 +6635,9 @@ var en = {
|
|
|
6352
6635
|
"inventory.usageAudit.columnCustomer": "Customer",
|
|
6353
6636
|
"inventory.usageAudit.columnCost": "Cost",
|
|
6354
6637
|
"inventory.usageAudit.standard": "Standard",
|
|
6638
|
+
"inventory.usageAudit.badgePosted": "Posted",
|
|
6639
|
+
"inventory.usageAudit.badgeApproved": "Approved",
|
|
6640
|
+
"inventory.usageAudit.batchNoStock": "{count} without stock \u2014 still queued",
|
|
6355
6641
|
"inventory.usageAudit.corrected": "Corrected",
|
|
6356
6642
|
"inventory.usageAudit.noLocation": "No position",
|
|
6357
6643
|
"inventory.usageAudit.unnamedProduct": "Unnamed product",
|
|
@@ -6359,6 +6645,22 @@ var en = {
|
|
|
6359
6645
|
"inventory.usageAudit.cancel": "Cancel",
|
|
6360
6646
|
"inventory.usageAudit.loadFailed": "Could not load the audit",
|
|
6361
6647
|
"inventory.usageAudit.searchPlaceholder": "Search by product, customer, professional\u2026",
|
|
6648
|
+
"inventory.usageAudit.periodLabel": "Period",
|
|
6649
|
+
"inventory.usageAudit.period30": "30 days",
|
|
6650
|
+
"inventory.usageAudit.period90": "90 days",
|
|
6651
|
+
"inventory.usageAudit.periodAll": "All time",
|
|
6652
|
+
"inventory.usageAudit.filterPending": "Pending",
|
|
6653
|
+
"inventory.usageAudit.emptyPending": "No usage waiting for review in this period.",
|
|
6654
|
+
"inventory.usageAudit.columnActions": "Actions",
|
|
6655
|
+
"inventory.usageAudit.post": "Post",
|
|
6656
|
+
"inventory.usageAudit.approve": "Approve",
|
|
6657
|
+
"inventory.usageAudit.postHint": "Accepts the usage AND deducts stock",
|
|
6658
|
+
"inventory.usageAudit.approveHint": "Accepts the usage without touching stock",
|
|
6659
|
+
"inventory.usageAudit.posted": "{{count}} line(s) posted to stock",
|
|
6660
|
+
"inventory.usageAudit.approved": "{{count}} line(s) approved",
|
|
6661
|
+
"inventory.usageAudit.approveFailed": "Could not accept the usage",
|
|
6662
|
+
"inventory.usageAudit.noStock": "No stock to post \u2014 receive the product or use Approve",
|
|
6663
|
+
"inventory.usageAudit.filtersLabel": "Filters",
|
|
6362
6664
|
"inventory.usageAudit.filterLabel": "Review",
|
|
6363
6665
|
"inventory.usageAudit.filterAll": "All",
|
|
6364
6666
|
"inventory.usageAudit.filterDivergent": "Off standard",
|
|
@@ -6540,6 +6842,10 @@ var ptBR = {
|
|
|
6540
6842
|
"inventory.quickActions.stockExitDesc": "Registrar mercadorias usadas ou vendidas",
|
|
6541
6843
|
"inventory.settingsPage.title": "Configura\xE7\xF5es de Estoque",
|
|
6542
6844
|
"inventory.settingsPage.subtitle": "Gerenciar unidades, categorias e locais de estoque",
|
|
6845
|
+
"inventory.settings.modules": "M\xF3dulos",
|
|
6846
|
+
"inventory.settings.modulesDesc": "O que desta casa aparece no Estoque",
|
|
6847
|
+
"inventory.settings.recipes": "Receitas",
|
|
6848
|
+
"inventory.settings.recipesDesc": "A ficha t\xE9cnica que diz quanto cada produto consome",
|
|
6543
6849
|
"inventory.settings.stockManagement": "Gest\xE3o de Estoque",
|
|
6544
6850
|
"inventory.settings.stockManagementDesc": "Como os n\xEDveis de estoque s\xE3o rastreados",
|
|
6545
6851
|
"inventory.settings.lowStockAlerts": "Alertas de estoque baixo",
|
|
@@ -6805,6 +7111,9 @@ var ptBR = {
|
|
|
6805
7111
|
"inventory.usageAudit.columnCustomer": "Cliente",
|
|
6806
7112
|
"inventory.usageAudit.columnCost": "Custo",
|
|
6807
7113
|
"inventory.usageAudit.standard": "Padr\xE3o",
|
|
7114
|
+
"inventory.usageAudit.badgePosted": "Baixado",
|
|
7115
|
+
"inventory.usageAudit.badgeApproved": "Aprovado",
|
|
7116
|
+
"inventory.usageAudit.batchNoStock": "{count} sem saldo \u2014 seguem na fila",
|
|
6808
7117
|
"inventory.usageAudit.corrected": "Corrigido",
|
|
6809
7118
|
"inventory.usageAudit.noLocation": "Sem posi\xE7\xE3o",
|
|
6810
7119
|
"inventory.usageAudit.unnamedProduct": "Produto sem nome",
|
|
@@ -6812,6 +7121,22 @@ var ptBR = {
|
|
|
6812
7121
|
"inventory.usageAudit.cancel": "Cancelar",
|
|
6813
7122
|
"inventory.usageAudit.loadFailed": "N\xE3o foi poss\xEDvel carregar a auditoria",
|
|
6814
7123
|
"inventory.usageAudit.searchPlaceholder": "Buscar por produto, cliente, profissional\u2026",
|
|
7124
|
+
"inventory.usageAudit.periodLabel": "Per\xEDodo",
|
|
7125
|
+
"inventory.usageAudit.period30": "30 dias",
|
|
7126
|
+
"inventory.usageAudit.period90": "90 dias",
|
|
7127
|
+
"inventory.usageAudit.periodAll": "Todo o per\xEDodo",
|
|
7128
|
+
"inventory.usageAudit.filterPending": "Pendentes",
|
|
7129
|
+
"inventory.usageAudit.emptyPending": "Nenhum consumo esperando confer\xEAncia neste per\xEDodo.",
|
|
7130
|
+
"inventory.usageAudit.columnActions": "A\xE7\xF5es",
|
|
7131
|
+
"inventory.usageAudit.post": "Baixar",
|
|
7132
|
+
"inventory.usageAudit.approve": "Aprovar",
|
|
7133
|
+
"inventory.usageAudit.postHint": "Aceita o consumo E desconta do saldo",
|
|
7134
|
+
"inventory.usageAudit.approveHint": "Aceita o consumo sem mexer no saldo",
|
|
7135
|
+
"inventory.usageAudit.posted": "{{count}} linha(s) baixada(s) do estoque",
|
|
7136
|
+
"inventory.usageAudit.approved": "{{count}} linha(s) aprovada(s)",
|
|
7137
|
+
"inventory.usageAudit.approveFailed": "N\xE3o foi poss\xEDvel aceitar o consumo",
|
|
7138
|
+
"inventory.usageAudit.noStock": "Sem saldo para baixar \u2014 d\xEA entrada no produto ou use Aprovar",
|
|
7139
|
+
"inventory.usageAudit.filtersLabel": "Filtros",
|
|
6815
7140
|
"inventory.usageAudit.filterLabel": "Confer\xEAncia",
|
|
6816
7141
|
"inventory.usageAudit.filterAll": "Tudo",
|
|
6817
7142
|
"inventory.usageAudit.filterDivergent": "Fora do padr\xE3o",
|
|
@@ -11381,13 +11706,3604 @@ VALUES ('stock_positions',
|
|
|
11381
11706
|
-- Esta linha \xE9 global (sem tenant), ent\xE3o \xE9 o \xEDndice global que ela quer.
|
|
11382
11707
|
ON CONFLICT (source_table) WHERE tenant_id IS NULL DO UPDATE SET reason = EXCLUDED.reason;
|
|
11383
11708
|
`;
|
|
11709
|
+
var MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO = `-- A produ\xE7\xE3o tem ordem e esta\xE7\xE3o.
|
|
11710
|
+
--
|
|
11711
|
+
-- O Pertinho do C\xE9u produz: pega insumo, passa por etapas, sai produto. Seis
|
|
11712
|
+
-- tabelas do V1 descrevem isso e nenhuma tinha destino.
|
|
11713
|
+
--
|
|
11714
|
+
-- production_orders 3 a ordem: o que produzir, quanto, quando
|
|
11715
|
+
-- production_order_inputs 27 o insumo consumido
|
|
11716
|
+
-- production_order_outputs 2 o que saiu
|
|
11717
|
+
-- production_order_steps 7 a etapa, na esta\xE7\xE3o, com tempo
|
|
11718
|
+
-- production_plan_items 2 o plano do dia, antes de virar ordem
|
|
11719
|
+
-- production_stations 4 onde a etapa acontece
|
|
11720
|
+
--
|
|
11721
|
+
-- O volume \xE9 rid\xEDculo \u2014 45 linhas \u2014 e \xE9 exatamente por isso que vale trazer: a
|
|
11722
|
+
-- casa est\xE1 COME\xC7ANDO a usar produ\xE7\xE3o, e o dia em que ela usar de verdade o
|
|
11723
|
+
-- hist\xF3rico j\xE1 vai estar do lado certo. Reconstituir depois \xE9 imposs\xEDvel: uma
|
|
11724
|
+
-- ordem de produ\xE7\xE3o fechada diz quanto de insumo virou quanto de produto, e
|
|
11725
|
+
-- esse n\xFAmero n\xE3o se recalcula.
|
|
11726
|
+
--
|
|
11727
|
+
-- \u2500\u2500 A receita j\xE1 existe, a ordem n\xE3o existia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11728
|
+
--
|
|
11729
|
+
-- \`plg_inventory_recipes\`, \`_recipe_ingredients\` e \`_recipe_groups\` j\xE1 est\xE3o no
|
|
11730
|
+
-- V2: \xE9 a FICHA, o que a coisa leva. O que faltava \xE9 a EXECU\xC7\xC3O da ficha \u2014 a
|
|
11731
|
+
-- ordem que a aplica num dia, com o que realmente entrou e o que realmente
|
|
11732
|
+
-- saiu. \`recipe_snapshot\` guarda a ficha como ela era no dia, porque mudar a
|
|
11733
|
+
-- receita amanh\xE3 n\xE3o pode reescrever a produ\xE7\xE3o de ontem.
|
|
11734
|
+
--
|
|
11735
|
+
-- \u2500\u2500 A esta\xE7\xE3o \xE9 do KDS tamb\xE9m \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11736
|
+
--
|
|
11737
|
+
-- \`production_stations\` do V1 carrega sete colunas de KDS (\`kds_enabled\`,
|
|
11738
|
+
-- \`kds_role\`, \`kds_sla_minutes\`\u2026). O V2 j\xE1 tem \`plg_kds_stations\` para isso, e
|
|
11739
|
+
-- duplicar a configura\xE7\xE3o do monitor aqui daria duas respostas para "qual a SLA
|
|
11740
|
+
-- desta esta\xE7\xE3o". Ent\xE3o elas v\xE3o para \`metadata.kds\`, de onde o dia da migra\xE7\xE3o
|
|
11741
|
+
-- do KDS as l\xEA \u2014 e a esta\xE7\xE3o de PRODU\xC7\xC3O fica com o que \xE9 dela: nome, unidade,
|
|
11742
|
+
-- se est\xE1 ativa.
|
|
11743
|
+
--
|
|
11744
|
+
-- Idempotente.
|
|
11745
|
+
|
|
11746
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_stations (
|
|
11747
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11748
|
+
tenant_id uuid NOT NULL,
|
|
11749
|
+
unit_id uuid,
|
|
11750
|
+
name text NOT NULL,
|
|
11751
|
+
description text,
|
|
11752
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
11753
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
11754
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11755
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11756
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
11757
|
+
);
|
|
11758
|
+
|
|
11759
|
+
COMMENT ON TABLE public.plg_inventory_production_stations IS
|
|
11760
|
+
'Onde a etapa de produ\xE7\xE3o acontece. V1: production_stations \u2014 a configura\xE7\xE3o de KDS dela fica em metadata.kds, porque plg_kds_stations \xE9 quem responde por monitor.';
|
|
11761
|
+
|
|
11762
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_orders (
|
|
11763
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11764
|
+
tenant_id uuid NOT NULL,
|
|
11765
|
+
unit_id uuid,
|
|
11766
|
+
order_number text,
|
|
11767
|
+
status text DEFAULT 'draft'::text NOT NULL,
|
|
11768
|
+
product_id uuid,
|
|
11769
|
+
recipe_id uuid,
|
|
11770
|
+
-- A ficha como ela era no dia. Mudar a receita amanh\xE3 n\xE3o reescreve a
|
|
11771
|
+
-- produ\xE7\xE3o de ontem.
|
|
11772
|
+
recipe_snapshot jsonb,
|
|
11773
|
+
planned_quantity numeric(18,4),
|
|
11774
|
+
measurement_unit_id uuid,
|
|
11775
|
+
planned_start timestamp with time zone,
|
|
11776
|
+
planned_end timestamp with time zone,
|
|
11777
|
+
actual_start timestamp with time zone,
|
|
11778
|
+
actual_end timestamp with time zone,
|
|
11779
|
+
source_type text,
|
|
11780
|
+
priority integer DEFAULT 0 NOT NULL,
|
|
11781
|
+
notes text,
|
|
11782
|
+
owner_id uuid,
|
|
11783
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11784
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11785
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11786
|
+
CONSTRAINT plg_inventory_production_orders_window_check
|
|
11787
|
+
CHECK (actual_start IS NULL OR actual_end IS NULL OR actual_end >= actual_start)
|
|
11788
|
+
);
|
|
11789
|
+
|
|
11790
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_steps (
|
|
11791
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11792
|
+
tenant_id uuid NOT NULL,
|
|
11793
|
+
production_order_id uuid NOT NULL,
|
|
11794
|
+
station_id uuid,
|
|
11795
|
+
sequence integer DEFAULT 0 NOT NULL,
|
|
11796
|
+
name text,
|
|
11797
|
+
instructions text,
|
|
11798
|
+
estimated_minutes integer,
|
|
11799
|
+
status text DEFAULT 'pending'::text NOT NULL,
|
|
11800
|
+
started_at timestamp with time zone,
|
|
11801
|
+
completed_at timestamp with time zone,
|
|
11802
|
+
-- O V1 guarda o nome do operador em texto, sem FK. Fica texto: inventar uma
|
|
11803
|
+
-- pessoa a partir de um nome digitado \xE9 como se cria cadastro duplicado.
|
|
11804
|
+
operator_name text,
|
|
11805
|
+
notes text,
|
|
11806
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11807
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11808
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
11809
|
+
);
|
|
11810
|
+
|
|
11811
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_inputs (
|
|
11812
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11813
|
+
tenant_id uuid NOT NULL,
|
|
11814
|
+
production_order_id uuid NOT NULL,
|
|
11815
|
+
step_id uuid,
|
|
11816
|
+
product_id uuid NOT NULL,
|
|
11817
|
+
-- Planejado contra real: \xE9 a diferen\xE7a entre os dois que diz se a ficha
|
|
11818
|
+
-- est\xE1 certa. Guardar s\xF3 o real perderia a pergunta.
|
|
11819
|
+
planned_quantity numeric(18,4),
|
|
11820
|
+
actual_quantity numeric(18,4),
|
|
11821
|
+
measurement_unit_id uuid,
|
|
11822
|
+
stock_position_id uuid,
|
|
11823
|
+
unit_cost numeric(18,6),
|
|
11824
|
+
consumed_at timestamp with time zone,
|
|
11825
|
+
operator_name text,
|
|
11826
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11827
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11828
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
11829
|
+
);
|
|
11830
|
+
|
|
11831
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_outputs (
|
|
11832
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11833
|
+
tenant_id uuid NOT NULL,
|
|
11834
|
+
production_order_id uuid NOT NULL,
|
|
11835
|
+
product_id uuid NOT NULL,
|
|
11836
|
+
-- Principal, subproduto ou perda. \xC9 a palavra que separa rendimento de
|
|
11837
|
+
-- desperd\xEDcio, e sem ela a conta de custo n\xE3o fecha.
|
|
11838
|
+
output_type text DEFAULT 'main'::text NOT NULL,
|
|
11839
|
+
planned_quantity numeric(18,4),
|
|
11840
|
+
actual_quantity numeric(18,4),
|
|
11841
|
+
measurement_unit_id uuid,
|
|
11842
|
+
batch_number text,
|
|
11843
|
+
expiration_date date,
|
|
11844
|
+
stock_position_id uuid,
|
|
11845
|
+
unit_cost numeric(18,6),
|
|
11846
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11847
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11848
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
11849
|
+
);
|
|
11850
|
+
|
|
11851
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_production_plan_items (
|
|
11852
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
11853
|
+
tenant_id uuid NOT NULL,
|
|
11854
|
+
unit_id uuid,
|
|
11855
|
+
plan_date date NOT NULL,
|
|
11856
|
+
product_id uuid NOT NULL,
|
|
11857
|
+
planned_quantity numeric(18,4),
|
|
11858
|
+
measurement_unit_id uuid,
|
|
11859
|
+
status text DEFAULT 'planned'::text NOT NULL,
|
|
11860
|
+
-- A ordem que o plano virou, quando virou. Nulo \xE9 plano ainda n\xE3o liberado.
|
|
11861
|
+
production_order_id uuid,
|
|
11862
|
+
notes text,
|
|
11863
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
11864
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
11865
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
11866
|
+
);
|
|
11867
|
+
|
|
11868
|
+
COMMENT ON TABLE public.plg_inventory_production_plan_items IS
|
|
11869
|
+
'O que se pretende produzir naquele dia, antes de virar ordem. V1: production_plan_items.';
|
|
11870
|
+
|
|
11871
|
+
-- \u2500\u2500 chaves \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11872
|
+
DO $$
|
|
11873
|
+
DECLARE t text;
|
|
11874
|
+
BEGIN
|
|
11875
|
+
FOREACH t IN ARRAY ARRAY['plg_inventory_production_stations', 'plg_inventory_production_orders',
|
|
11876
|
+
'plg_inventory_production_steps', 'plg_inventory_production_inputs',
|
|
11877
|
+
'plg_inventory_production_outputs', 'plg_inventory_production_plan_items'] LOOP
|
|
11878
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN
|
|
11879
|
+
EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');
|
|
11880
|
+
END IF;
|
|
11881
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN
|
|
11882
|
+
EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');
|
|
11883
|
+
END IF;
|
|
11884
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN
|
|
11885
|
+
EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');
|
|
11886
|
+
END IF;
|
|
11887
|
+
END LOOP;
|
|
11888
|
+
END $$;
|
|
11889
|
+
|
|
11890
|
+
DO $$ BEGIN
|
|
11891
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_stations_unit_fkey') THEN
|
|
11892
|
+
ALTER TABLE ONLY public.plg_inventory_production_stations ADD CONSTRAINT plg_inventory_production_stations_unit_fkey
|
|
11893
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
|
|
11894
|
+
END IF;
|
|
11895
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_unit_fkey') THEN
|
|
11896
|
+
ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_unit_fkey
|
|
11897
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
|
|
11898
|
+
END IF;
|
|
11899
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_product_fkey') THEN
|
|
11900
|
+
ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_product_fkey
|
|
11901
|
+
FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;
|
|
11902
|
+
END IF;
|
|
11903
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_recipe_fkey') THEN
|
|
11904
|
+
ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_recipe_fkey
|
|
11905
|
+
FOREIGN KEY (recipe_id) REFERENCES public.plg_inventory_recipes(id) ON DELETE SET NULL;
|
|
11906
|
+
END IF;
|
|
11907
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_uom_fkey') THEN
|
|
11908
|
+
ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_uom_fkey
|
|
11909
|
+
FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;
|
|
11910
|
+
END IF;
|
|
11911
|
+
|
|
11912
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_steps_order_fkey') THEN
|
|
11913
|
+
ALTER TABLE ONLY public.plg_inventory_production_steps ADD CONSTRAINT plg_inventory_production_steps_order_fkey
|
|
11914
|
+
FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;
|
|
11915
|
+
END IF;
|
|
11916
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_steps_station_fkey') THEN
|
|
11917
|
+
ALTER TABLE ONLY public.plg_inventory_production_steps ADD CONSTRAINT plg_inventory_production_steps_station_fkey
|
|
11918
|
+
FOREIGN KEY (station_id) REFERENCES public.plg_inventory_production_stations(id) ON DELETE SET NULL;
|
|
11919
|
+
END IF;
|
|
11920
|
+
|
|
11921
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_order_fkey') THEN
|
|
11922
|
+
ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_order_fkey
|
|
11923
|
+
FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;
|
|
11924
|
+
END IF;
|
|
11925
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_step_fkey') THEN
|
|
11926
|
+
ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_step_fkey
|
|
11927
|
+
FOREIGN KEY (step_id) REFERENCES public.plg_inventory_production_steps(id) ON DELETE SET NULL;
|
|
11928
|
+
END IF;
|
|
11929
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_product_fkey') THEN
|
|
11930
|
+
ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_product_fkey
|
|
11931
|
+
FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
|
|
11932
|
+
END IF;
|
|
11933
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_uom_fkey') THEN
|
|
11934
|
+
ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_uom_fkey
|
|
11935
|
+
FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;
|
|
11936
|
+
END IF;
|
|
11937
|
+
|
|
11938
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_order_fkey') THEN
|
|
11939
|
+
ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_order_fkey
|
|
11940
|
+
FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;
|
|
11941
|
+
END IF;
|
|
11942
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_product_fkey') THEN
|
|
11943
|
+
ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_product_fkey
|
|
11944
|
+
FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
|
|
11945
|
+
END IF;
|
|
11946
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_uom_fkey') THEN
|
|
11947
|
+
ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_uom_fkey
|
|
11948
|
+
FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;
|
|
11949
|
+
END IF;
|
|
11950
|
+
|
|
11951
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_unit_fkey') THEN
|
|
11952
|
+
ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_unit_fkey
|
|
11953
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
|
|
11954
|
+
END IF;
|
|
11955
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_product_fkey') THEN
|
|
11956
|
+
ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_product_fkey
|
|
11957
|
+
FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
|
|
11958
|
+
END IF;
|
|
11959
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_order_fkey') THEN
|
|
11960
|
+
ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_order_fkey
|
|
11961
|
+
FOREIGN KEY (production_order_id) REFERENCES public.plg_inventory_production_orders(id) ON DELETE SET NULL;
|
|
11962
|
+
END IF;
|
|
11963
|
+
END $$;
|
|
11964
|
+
|
|
11965
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_inventory_production_orders_number_uidx
|
|
11966
|
+
ON public.plg_inventory_production_orders USING btree (tenant_id, order_number)
|
|
11967
|
+
WHERE order_number IS NOT NULL;
|
|
11968
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_orders_open_idx
|
|
11969
|
+
ON public.plg_inventory_production_orders USING btree (tenant_id, unit_id, status, planned_start);
|
|
11970
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_steps_order_idx
|
|
11971
|
+
ON public.plg_inventory_production_steps USING btree (tenant_id, production_order_id, sequence);
|
|
11972
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_inputs_order_idx
|
|
11973
|
+
ON public.plg_inventory_production_inputs USING btree (tenant_id, production_order_id);
|
|
11974
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_inputs_product_idx
|
|
11975
|
+
ON public.plg_inventory_production_inputs USING btree (tenant_id, product_id, consumed_at DESC);
|
|
11976
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_outputs_order_idx
|
|
11977
|
+
ON public.plg_inventory_production_outputs USING btree (tenant_id, production_order_id);
|
|
11978
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_production_plan_items_day_idx
|
|
11979
|
+
ON public.plg_inventory_production_plan_items USING btree (tenant_id, unit_id, plan_date);
|
|
11980
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_inventory_production_stations_name_uidx
|
|
11981
|
+
ON public.plg_inventory_production_stations USING btree (tenant_id, unit_id, lower(name))
|
|
11982
|
+
WHERE unit_id IS NOT NULL;
|
|
11983
|
+
|
|
11984
|
+
-- RLS, uma a uma e por extenso. O la\xE7o adiante j\xE1 liga tudo isto \u2014 mas ele liga
|
|
11985
|
+
-- por \`EXECUTE format(...)\`, e o port\xE3o que revisa plugin l\xEA o ARQUIVO, n\xE3o o
|
|
11986
|
+
-- banco: para ele, tabela com tenant_id e sem um ENABLE literal \xE9 tabela
|
|
11987
|
+
-- aberta. Uma garantia que o revisor n\xE3o consegue ler \xE9 uma garantia que
|
|
11988
|
+
-- ningu\xE9m confere.
|
|
11989
|
+
ALTER TABLE public.plg_inventory_production_stations ENABLE ROW LEVEL SECURITY;
|
|
11990
|
+
ALTER TABLE public.plg_inventory_production_orders ENABLE ROW LEVEL SECURITY;
|
|
11991
|
+
ALTER TABLE public.plg_inventory_production_steps ENABLE ROW LEVEL SECURITY;
|
|
11992
|
+
ALTER TABLE public.plg_inventory_production_inputs ENABLE ROW LEVEL SECURITY;
|
|
11993
|
+
ALTER TABLE public.plg_inventory_production_outputs ENABLE ROW LEVEL SECURITY;
|
|
11994
|
+
ALTER TABLE public.plg_inventory_production_plan_items ENABLE ROW LEVEL SECURITY;
|
|
11995
|
+
|
|
11996
|
+
DO $$
|
|
11997
|
+
DECLARE t text;
|
|
11998
|
+
DECLARE tem_unidade boolean;
|
|
11999
|
+
BEGIN
|
|
12000
|
+
FOREACH t IN ARRAY ARRAY['plg_inventory_production_stations', 'plg_inventory_production_orders',
|
|
12001
|
+
'plg_inventory_production_steps', 'plg_inventory_production_inputs',
|
|
12002
|
+
'plg_inventory_production_outputs', 'plg_inventory_production_plan_items'] LOOP
|
|
12003
|
+
tem_unidade := t IN ('plg_inventory_production_stations', 'plg_inventory_production_orders',
|
|
12004
|
+
'plg_inventory_production_plan_items');
|
|
12005
|
+
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);
|
|
12006
|
+
EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);
|
|
12007
|
+
EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);
|
|
12008
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
|
|
12009
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);
|
|
12010
|
+
EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);
|
|
12011
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);
|
|
12012
|
+
EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (
|
|
12013
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
12014
|
+
AND (%s)
|
|
12015
|
+
AND (SELECT app.has_permission('inventory.read', %s)))$f$,
|
|
12016
|
+
t || '_authz_select', t,
|
|
12017
|
+
CASE WHEN tem_unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,
|
|
12018
|
+
CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);
|
|
12019
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_write', t);
|
|
12020
|
+
EXECUTE format($f$CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (
|
|
12021
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
12022
|
+
AND (SELECT app.has_permission('inventory.edit', %s)))
|
|
12023
|
+
WITH CHECK (
|
|
12024
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
12025
|
+
AND (SELECT app.has_permission('inventory.edit', %s)))$f$,
|
|
12026
|
+
t || '_authz_write', t,
|
|
12027
|
+
CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,
|
|
12028
|
+
CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);
|
|
12029
|
+
END LOOP;
|
|
12030
|
+
END $$;
|
|
12031
|
+
|
|
12032
|
+
INSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)
|
|
12033
|
+
VALUES ('plg_inventory_production_orders', 'unit_id', 'owner_id', false, 'unit', 'Ordens de produ\xE7\xE3o')
|
|
12034
|
+
ON CONFLICT (resource_table) DO UPDATE
|
|
12035
|
+
SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;
|
|
12036
|
+
`;
|
|
12037
|
+
var MIGRATION_007_A_PRODUCAO_ATRAVESSA = `-- A produ\xE7\xE3o atravessa.
|
|
12038
|
+
--
|
|
12039
|
+
-- A 006 fez as seis tabelas. Esta escreve o de-para com \`by_shape\` (spine 123).
|
|
12040
|
+
-- A ordem importa: esta\xE7\xE3o antes da etapa, ordem antes de tudo que pendura
|
|
12041
|
+
-- nela, e o plano por \xFAltimo porque ele aponta para a ordem que gerou.
|
|
12042
|
+
--
|
|
12043
|
+
-- \u2500\u2500 A configura\xE7\xE3o de KDS n\xE3o vem para c\xE1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12044
|
+
--
|
|
12045
|
+
-- \`production_stations\` carrega sete colunas de KDS (\`kds_enabled\`, \`kds_role\`,
|
|
12046
|
+
-- \`kds_sla_minutes\`\u2026). Elas v\xE3o para \`metadata.kds\` e n\xE3o para coluna: o V2 tem
|
|
12047
|
+
-- \`plg_kds_stations\`, e duplicar a configura\xE7\xE3o do monitor aqui daria duas
|
|
12048
|
+
-- respostas para "qual a SLA desta esta\xE7\xE3o". Quando o KDS portar, ele l\xEA o
|
|
12049
|
+
-- metadata e a esta\xE7\xE3o de produ\xE7\xE3o fica com o que \xE9 dela.
|
|
12050
|
+
--
|
|
12051
|
+
-- \u2500\u2500 O movimento de estoque fica como refer\xEAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12052
|
+
--
|
|
12053
|
+
-- \`stock_movement_uuid\` no insumo e no produto aponta para a baixa/entrada que
|
|
12054
|
+
-- a produ\xE7\xE3o gerou. Vem como refer\xEAncia em metadata, n\xE3o como FK: o movimento
|
|
12055
|
+
-- atravessa por outro caminho (\`stock_movements\`) e amarrar os dois aqui
|
|
12056
|
+
-- exigiria que a ordem de produ\xE7\xE3o esperasse o raz\xE3o \u2014 uma depend\xEAncia que o
|
|
12057
|
+
-- volume (45 linhas) n\xE3o justifica.
|
|
12058
|
+
--
|
|
12059
|
+
-- Idempotente.
|
|
12060
|
+
|
|
12061
|
+
SET LOCAL migration.allowlist_unlock = 'on';
|
|
12062
|
+
|
|
12063
|
+
SELECT migration._allow(
|
|
12064
|
+
'production_stations', 'public.plg_inventory_production_stations', 'inventory', 'by_shape',
|
|
12065
|
+
jsonb_build_object('target', 'public.plg_inventory_production_stations'),
|
|
12066
|
+
NULL::text[],
|
|
12067
|
+
jsonb_build_object(
|
|
12068
|
+
'uuid', 'id',
|
|
12069
|
+
'name', 'name',
|
|
12070
|
+
'description', 'description',
|
|
12071
|
+
'active', 'is_active',
|
|
12072
|
+
'kds_display_order', 'sort_order',
|
|
12073
|
+
'created_at', 'created_at',
|
|
12074
|
+
'updated_at', 'updated_at',
|
|
12075
|
+
'tenant_id', '-',
|
|
12076
|
+
'unit_id', '-',
|
|
12077
|
+
'kds_enabled', 'jsonb:kds.enabled',
|
|
12078
|
+
'kds_role', 'jsonb:kds.role',
|
|
12079
|
+
'is_kds_fallback', 'jsonb:kds.is_fallback',
|
|
12080
|
+
'kds_sla_minutes', 'jsonb:kds.sla_minutes',
|
|
12081
|
+
'kds_catalog_mode', 'jsonb:kds.catalog_mode',
|
|
12082
|
+
'kds_config_version', 'jsonb:kds.config_version',
|
|
12083
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12084
|
+
'Onde a etapa acontece. \`unit_id\` da origem \xE9 uuid de companies e n\xE3o o id '
|
|
12085
|
+
'num\xE9rico que o ledger resolve \u2014 a unidade vem pelo augment do extrator. A '
|
|
12086
|
+
'configura\xE7\xE3o de KDS fica em metadata.kds porque plg_kds_stations \xE9 quem '
|
|
12087
|
+
'responde por monitor.'
|
|
12088
|
+
);
|
|
12089
|
+
|
|
12090
|
+
SELECT migration._allow(
|
|
12091
|
+
'production_orders', 'public.plg_inventory_production_orders', 'inventory', 'by_shape',
|
|
12092
|
+
jsonb_build_object('target', 'public.plg_inventory_production_orders'),
|
|
12093
|
+
NULL::text[],
|
|
12094
|
+
jsonb_build_object(
|
|
12095
|
+
'uuid', 'id',
|
|
12096
|
+
'unit_id', 'unit_id@companies',
|
|
12097
|
+
'order_number', 'order_number',
|
|
12098
|
+
'status', 'status',
|
|
12099
|
+
'product_id', 'product_id@products',
|
|
12100
|
+
'recipe_id', 'recipe_id@recipes',
|
|
12101
|
+
'recipe_snapshot', 'recipe_snapshot',
|
|
12102
|
+
'planned_qty', 'planned_quantity',
|
|
12103
|
+
'unit_id_measure', 'measurement_unit_id@measurement_units',
|
|
12104
|
+
'planned_start', 'planned_start',
|
|
12105
|
+
'planned_end', 'planned_end',
|
|
12106
|
+
'actual_start', 'actual_start',
|
|
12107
|
+
'actual_end', 'actual_end',
|
|
12108
|
+
'source_type', 'source_type',
|
|
12109
|
+
'priority', 'priority',
|
|
12110
|
+
'notes', 'notes',
|
|
12111
|
+
'created_at', 'created_at',
|
|
12112
|
+
'updated_at', 'updated_at',
|
|
12113
|
+
'tenant_id', '-',
|
|
12114
|
+
'created_by', '-',
|
|
12115
|
+
'released_by', '-',
|
|
12116
|
+
'finished_by', '-',
|
|
12117
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12118
|
+
'A ordem de produ\xE7\xE3o. \`recipe_snapshot\` \xE9 a ficha como ela era no dia \u2014 '
|
|
12119
|
+
'mudar a receita amanh\xE3 n\xE3o pode reescrever a produ\xE7\xE3o de ontem.'
|
|
12120
|
+
);
|
|
12121
|
+
|
|
12122
|
+
SELECT migration._allow(
|
|
12123
|
+
'production_order_steps', 'public.plg_inventory_production_steps', 'inventory', 'by_shape',
|
|
12124
|
+
jsonb_build_object('target', 'public.plg_inventory_production_steps'),
|
|
12125
|
+
NULL::text[],
|
|
12126
|
+
jsonb_build_object(
|
|
12127
|
+
'uuid', 'id',
|
|
12128
|
+
'order_id', 'production_order_id@production_orders',
|
|
12129
|
+
'production_station_id', 'station_id@production_stations',
|
|
12130
|
+
'sequence', 'sequence',
|
|
12131
|
+
'name', 'name',
|
|
12132
|
+
'instructions', 'instructions',
|
|
12133
|
+
'estimated_minutes', 'estimated_minutes',
|
|
12134
|
+
'status', 'status',
|
|
12135
|
+
'started_at', 'started_at',
|
|
12136
|
+
'completed_at', 'completed_at',
|
|
12137
|
+
'operator_name', 'operator_name',
|
|
12138
|
+
'notes', 'notes',
|
|
12139
|
+
'created_at', 'created_at',
|
|
12140
|
+
'updated_at', 'updated_at',
|
|
12141
|
+
'tenant_id', '-',
|
|
12142
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12143
|
+
'A etapa, na esta\xE7\xE3o, com tempo. \`operator_name\` continua texto: inventar uma '
|
|
12144
|
+
'pessoa a partir de um nome digitado \xE9 como se cria cadastro duplicado.'
|
|
12145
|
+
);
|
|
12146
|
+
|
|
12147
|
+
SELECT migration._allow(
|
|
12148
|
+
'production_order_inputs', 'public.plg_inventory_production_inputs', 'inventory', 'by_shape',
|
|
12149
|
+
jsonb_build_object('target', 'public.plg_inventory_production_inputs'),
|
|
12150
|
+
NULL::text[],
|
|
12151
|
+
jsonb_build_object(
|
|
12152
|
+
'uuid', 'id',
|
|
12153
|
+
'order_id', 'production_order_id@production_orders',
|
|
12154
|
+
'step_id', 'step_id@production_order_steps',
|
|
12155
|
+
'product_id', 'product_id@products',
|
|
12156
|
+
'planned_qty', 'planned_quantity',
|
|
12157
|
+
'actual_qty', 'actual_quantity',
|
|
12158
|
+
'unit_id', 'measurement_unit_id@measurement_units',
|
|
12159
|
+
'stock_position_id', 'stock_position_id@stock_positions',
|
|
12160
|
+
'unit_cost', 'unit_cost',
|
|
12161
|
+
'consumed_at', 'consumed_at',
|
|
12162
|
+
'operator_name', 'operator_name',
|
|
12163
|
+
'created_at', 'created_at',
|
|
12164
|
+
'updated_at', 'updated_at',
|
|
12165
|
+
'tenant_id', '-',
|
|
12166
|
+
'product_uuid', '-',
|
|
12167
|
+
'unit_label_id', '-',
|
|
12168
|
+
'stock_movement_uuid', 'jsonb:inventory.v1_stock_movement_ref',
|
|
12169
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12170
|
+
'O insumo consumido. Planejado E real: \xE9 a diferen\xE7a entre os dois que diz se '
|
|
12171
|
+
'a ficha est\xE1 certa. \`unit_id\` aqui \xE9 UNIDADE DE MEDIDA, n\xE3o filial \u2014 o nome '
|
|
12172
|
+
'da coluna do V1 engana, e por isso o destino se chama measurement_unit_id.'
|
|
12173
|
+
);
|
|
12174
|
+
|
|
12175
|
+
SELECT migration._allow(
|
|
12176
|
+
'production_order_outputs', 'public.plg_inventory_production_outputs', 'inventory', 'by_shape',
|
|
12177
|
+
jsonb_build_object('target', 'public.plg_inventory_production_outputs'),
|
|
12178
|
+
NULL::text[],
|
|
12179
|
+
jsonb_build_object(
|
|
12180
|
+
'uuid', 'id',
|
|
12181
|
+
'order_id', 'production_order_id@production_orders',
|
|
12182
|
+
'product_id', 'product_id@products',
|
|
12183
|
+
'output_type', 'output_type',
|
|
12184
|
+
'planned_qty', 'planned_quantity',
|
|
12185
|
+
'actual_qty', 'actual_quantity',
|
|
12186
|
+
'unit_id', 'measurement_unit_id@measurement_units',
|
|
12187
|
+
'batch_number', 'batch_number',
|
|
12188
|
+
'expiration_date', 'expiration_date',
|
|
12189
|
+
'stock_position_id', 'stock_position_id@stock_positions',
|
|
12190
|
+
'unit_cost', 'unit_cost',
|
|
12191
|
+
'created_at', 'created_at',
|
|
12192
|
+
'updated_at', 'updated_at',
|
|
12193
|
+
'tenant_id', '-',
|
|
12194
|
+
'product_uuid', '-',
|
|
12195
|
+
'stock_movement_uuid', 'jsonb:inventory.v1_stock_movement_ref',
|
|
12196
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12197
|
+
'O que saiu: principal, subproduto ou perda. \xC9 essa palavra que separa '
|
|
12198
|
+
'rendimento de desperd\xEDcio, e sem ela a conta de custo n\xE3o fecha.'
|
|
12199
|
+
);
|
|
12200
|
+
|
|
12201
|
+
SELECT migration._allow(
|
|
12202
|
+
'production_plan_items', 'public.plg_inventory_production_plan_items', 'inventory', 'by_shape',
|
|
12203
|
+
jsonb_build_object('target', 'public.plg_inventory_production_plan_items'),
|
|
12204
|
+
NULL::text[],
|
|
12205
|
+
jsonb_build_object(
|
|
12206
|
+
'uuid', 'id',
|
|
12207
|
+
'unit_id', 'unit_id@companies',
|
|
12208
|
+
'plan_date', 'plan_date',
|
|
12209
|
+
'product_id', 'product_id@products',
|
|
12210
|
+
'planned_qty', 'planned_quantity',
|
|
12211
|
+
'unit_id_measure', 'measurement_unit_id@measurement_units',
|
|
12212
|
+
'status', 'status',
|
|
12213
|
+
'production_order_id', 'production_order_id@production_orders',
|
|
12214
|
+
'notes', 'notes',
|
|
12215
|
+
'created_at', 'created_at',
|
|
12216
|
+
'updated_at', 'updated_at',
|
|
12217
|
+
'tenant_id', '-',
|
|
12218
|
+
'id', 'jsonb:inventory.v1_ref'),
|
|
12219
|
+
'O que se pretende produzir naquele dia, antes de virar ordem. Vem por \xFAltimo '
|
|
12220
|
+
'porque aponta para a ordem que ele gerou.'
|
|
12221
|
+
);
|
|
12222
|
+
|
|
12223
|
+
DELETE FROM migration.excluded_tables
|
|
12224
|
+
WHERE source_table IN ('production_stations', 'production_orders', 'production_order_steps',
|
|
12225
|
+
'production_order_inputs', 'production_order_outputs', 'production_plan_items');
|
|
12226
|
+
|
|
12227
|
+
RESET migration.allowlist_unlock;
|
|
12228
|
+
`;
|
|
12229
|
+
var MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA = `-- O grupo da receita tamb\xE9m \xE9 de uma casa.
|
|
12230
|
+
--
|
|
12231
|
+
-- \`plg_inventory_recipes\` e \`plg_inventory_recipe_ingredients\` j\xE1 carregam
|
|
12232
|
+
-- \`unit_id\`. \`plg_inventory_recipe_groups\` \u2014 que \xE9 irm\xE3o dos dois e agrupa
|
|
12233
|
+
-- ingredientes alternativos dentro da mesma ficha \u2014 ficou sem.
|
|
12234
|
+
--
|
|
12235
|
+
-- S\xE3o 172 grupos no Pertinho do C\xE9u. Sem a coluna, o grupo de uma cozinha
|
|
12236
|
+
-- aparece na ficha da outra, e a substitui\xE7\xE3o de ingrediente sugere um produto
|
|
12237
|
+
-- que aquela casa n\xE3o tem em estoque.
|
|
12238
|
+
--
|
|
12239
|
+
-- Idempotente.
|
|
12240
|
+
|
|
12241
|
+
ALTER TABLE public.plg_inventory_recipe_groups ADD COLUMN IF NOT EXISTS unit_id uuid;
|
|
12242
|
+
|
|
12243
|
+
DO $$ BEGIN
|
|
12244
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_recipe_groups_unit_fkey') THEN
|
|
12245
|
+
ALTER TABLE public.plg_inventory_recipe_groups ADD CONSTRAINT plg_inventory_recipe_groups_unit_fkey
|
|
12246
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
|
|
12247
|
+
END IF;
|
|
12248
|
+
END $$;
|
|
12249
|
+
|
|
12250
|
+
COMMENT ON COLUMN public.plg_inventory_recipe_groups.unit_id IS
|
|
12251
|
+
'A cozinha a que o grupo pertence \u2014 as irm\xE3s plg_inventory_recipes e _recipe_ingredients j\xE1 a tinham (008).';
|
|
12252
|
+
|
|
12253
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_recipe_groups_unit_idx
|
|
12254
|
+
ON public.plg_inventory_recipe_groups USING btree (tenant_id, unit_id);
|
|
12255
|
+
`;
|
|
12256
|
+
var MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO = `-- A dire\xE7\xE3o do movimento n\xE3o est\xE1 no n\xFAmero \u2014 e o V2 guarda ela l\xE1.
|
|
12257
|
+
--
|
|
12258
|
+
-- \`plg_inventory_stock_movements\` assina a quantidade. O CHECK diz isso em voz
|
|
12259
|
+
-- alta desde o baseline:
|
|
12260
|
+
--
|
|
12261
|
+
-- plg_inventory_stock_movements_sign_check
|
|
12262
|
+
-- (kind = 'in' AND quantity > 0)
|
|
12263
|
+
-- OR (kind = 'out' AND quantity < 0)
|
|
12264
|
+
-- OR (kind = 'transfer' AND quantity < 0)
|
|
12265
|
+
-- OR kind IN ('adjust', 'reverse')
|
|
12266
|
+
--
|
|
12267
|
+
-- E n\xE3o \xE9 decora\xE7\xE3o: \`app.inventory_movements_after_insert\` soma \`NEW.quantity\`
|
|
12268
|
+
-- na posi\xE7\xE3o de origem e \`-NEW.quantity\` na de destino. O sinal \xC9 o efeito no
|
|
12269
|
+
-- saldo. Uma sa\xEDda positiva n\xE3o seria recusada por capricho \u2014 ela AUMENTARIA o
|
|
12270
|
+
-- estoque.
|
|
12271
|
+
--
|
|
12272
|
+
-- O V1 guarda a dire\xE7\xE3o em outro lugar: no \`movement_type\`. O n\xFAmero \xE9 sempre a
|
|
12273
|
+
-- magnitude. Medido em 21/09/2026 sobre os 2.997 movimentos do banco inteiro,
|
|
12274
|
+
-- todos os inquilinos:
|
|
12275
|
+
--
|
|
12276
|
+
-- entrada 2.871 menor 0,001 maior 10.000 negativos: 0
|
|
12277
|
+
-- saida 119 menor 0,2 maior 26 negativos: 0
|
|
12278
|
+
-- ajuste 6 menor 1 maior 1 negativos: 0
|
|
12279
|
+
-- transferencia 1 menor 1 maior 1 negativos: 0
|
|
12280
|
+
--
|
|
12281
|
+
-- \`migration.upsert_inv_movement\` traduzia o R\xD3TULO (\`saida\` \u2192 \`out\`) e passava
|
|
12282
|
+
-- o n\xFAmero adiante intacto. Ou seja: toda sa\xEDda e toda transfer\xEAncia do V1 era
|
|
12283
|
+
-- uma viola\xE7\xE3o de constraint garantida. Isso ficou dois inquilinos sem
|
|
12284
|
+
-- aparecer porque o Pertinho do C\xE9u s\xF3 tem entrada \u2014 2.707 de 2.707. O Espa\xE7o
|
|
12285
|
+
-- Facial \xE9 o primeiro com sa\xEDda, e as tr\xEAs sa\xEDdas dele que tinham local
|
|
12286
|
+
-- bateram na constraint:
|
|
12287
|
+
--
|
|
12288
|
+
-- state: 23514: ... violates check constraint
|
|
12289
|
+
-- "plg_inventory_stock_movements_sign_check" \xD73
|
|
12290
|
+
--
|
|
12291
|
+
-- \u2500\u2500 O ajuste \xE9 o caso que n\xE3o se resolve pelo r\xF3tulo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12292
|
+
--
|
|
12293
|
+
-- \`entrada\`, \`saida\` e \`transferencia\` dizem a dire\xE7\xE3o no nome. \`ajuste\` n\xE3o:
|
|
12294
|
+
-- ele tanto acha quanto perde, e o V1 escreve 1 nos dois casos. Quem sabe \xE9 a
|
|
12295
|
+
-- contagem que o gerou \u2014 \`stock_count_items.counted_qty - system_qty_at_review\`
|
|
12296
|
+
-- \xE9 a diferen\xE7a apurada, com sinal. Conferido nos 4 ajustes de invent\xE1rio do
|
|
12297
|
+
-- Espa\xE7o Facial: os dois "falta apurada" d\xE3o -1, os dois "sobra apurada" d\xE3o
|
|
12298
|
+
-- +1. O extrator passou a mandar isso como \`adjust_delta\` (ver
|
|
12299
|
+
-- fayz-etl/fayz_etl/sources/beautysoft_v1.py, passo \`stock_movements\`), e o
|
|
12300
|
+
-- de-para abaixo abre a porta para a coluna.
|
|
12301
|
+
--
|
|
12302
|
+
-- Quando o ajuste n\xE3o vem de contagem, \`adjust_delta\` chega nulo \u2014 e a\xED a
|
|
12303
|
+
-- linha vai para a quarentena em vez de entrar com o sinal chutado. S\xE3o 2
|
|
12304
|
+
-- linhas hoje ("Corre\xE7\xE3o de auditoria (estorno)"), e as duas j\xE1 estavam
|
|
12305
|
+
-- paradas por falta de local. Somar onde era para subtrair \xE9 o erro que
|
|
12306
|
+
-- ningu\xE9m v\xEA: ele n\xE3o quebra nada, s\xF3 deixa o saldo errado.
|
|
12307
|
+
--
|
|
12308
|
+
-- \u2500\u2500 O que N\xC3O muda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12309
|
+
--
|
|
12310
|
+
-- A guarda de entrada continua exigindo \`quantity > 0\`: o writer recebe do V1,
|
|
12311
|
+
-- e no V1 a dire\xE7\xE3o \xE9 o tipo, n\xE3o o sinal. O que muda \xE9 a SA\xCDDA dele \u2014 a
|
|
12312
|
+
-- tradu\xE7\xE3o para a conven\xE7\xE3o da tabela de destino, que \xE9 onde ela sempre
|
|
12313
|
+
-- deveria ter acontecido. As 2.208 + 47 entradas j\xE1 migradas n\xE3o se movem: para
|
|
12314
|
+
-- \`in\`, assinado e magnitude s\xE3o o mesmo n\xFAmero, e a tabela \xE9 append-only.
|
|
12315
|
+
--
|
|
12316
|
+
-- Idempotente: CREATE OR REPLACE e um UPDATE de jsonb por concatena\xE7\xE3o.
|
|
12317
|
+
|
|
12318
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12319
|
+
-- 1. O writer assina o n\xFAmero
|
|
12320
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12321
|
+
|
|
12322
|
+
CREATE OR REPLACE FUNCTION migration.upsert_inv_movement(
|
|
12323
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
12324
|
+
) RETURNS jsonb
|
|
12325
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
12326
|
+
AS $function$
|
|
12327
|
+
DECLARE
|
|
12328
|
+
v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'kind', ''))), '');
|
|
12329
|
+
v_kind text := CASE v_kind_raw
|
|
12330
|
+
WHEN 'in' THEN 'in' WHEN 'out' THEN 'out'
|
|
12331
|
+
WHEN 'transfer' THEN 'transfer' WHEN 'adjust' THEN 'adjust' WHEN 'reverse' THEN 'reverse'
|
|
12332
|
+
-- the SDK's legacy labels, and V1's pt-BR
|
|
12333
|
+
WHEN 'entry' THEN 'in' WHEN 'entrada' THEN 'in'
|
|
12334
|
+
WHEN 'exit' THEN 'out' WHEN 'saida' THEN 'out' WHEN 'sa\xEDda' THEN 'out'
|
|
12335
|
+
WHEN 'loss' THEN 'out' WHEN 'perda' THEN 'out'
|
|
12336
|
+
WHEN 'adjustment' THEN 'adjust' WHEN 'ajuste' THEN 'adjust'
|
|
12337
|
+
WHEN 'transferencia' THEN 'transfer' WHEN 'transfer\xEAncia' THEN 'transfer'
|
|
12338
|
+
END;
|
|
12339
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
12340
|
+
-- A magnitude, como a origem a manda, e o n\xFAmero ASSINADO que a tabela pede.
|
|
12341
|
+
-- Os dois existem porque s\xE3o contratos de lados diferentes da fronteira.
|
|
12342
|
+
v_qty numeric := coalesce((p_row ->> 'quantity')::numeric, 0);
|
|
12343
|
+
v_delta numeric := (p_row ->> 'adjust_delta')::numeric;
|
|
12344
|
+
v_signed numeric;
|
|
12345
|
+
v_exists boolean;
|
|
12346
|
+
v_standalone boolean := migration._standalone();
|
|
12347
|
+
BEGIN
|
|
12348
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
12349
|
+
IF p_id IS NULL OR (p_row ->> 'product_id') IS NULL THEN
|
|
12350
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'a stock movement needs an id and its product');
|
|
12351
|
+
END IF;
|
|
12352
|
+
IF v_kind IS NULL THEN
|
|
12353
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
12354
|
+
'reason', format('unknown movement type %L (expected in|out|transfer|adjust|reverse, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));
|
|
12355
|
+
END IF;
|
|
12356
|
+
IF v_qty <= 0 THEN
|
|
12357
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a stock movement needs a quantity > 0 \u2014 direction is the kind, not the sign');
|
|
12358
|
+
END IF;
|
|
12359
|
+
IF v_kind = 'transfer' AND (p_row ->> 'destination_location_id') IS NULL THEN
|
|
12360
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a transfer needs its destination location');
|
|
12361
|
+
END IF;
|
|
12362
|
+
|
|
12363
|
+
-- A tradu\xE7\xE3o. O r\xF3tulo diz a dire\xE7\xE3o em tr\xEAs dos cinco tipos; no ajuste ela
|
|
12364
|
+
-- vem da contagem, e sem ela n\xE3o se inventa.
|
|
12365
|
+
IF v_kind = 'adjust' THEN
|
|
12366
|
+
IF v_delta IS NULL OR v_delta = 0 THEN
|
|
12367
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
12368
|
+
'reason', 'an adjustment does not say which way it goes \u2014 the V1 writes the size and leaves the sign to the count it came from (adjust_delta)');
|
|
12369
|
+
END IF;
|
|
12370
|
+
v_signed := sign(v_delta) * v_qty;
|
|
12371
|
+
ELSIF v_kind IN ('out', 'transfer') THEN
|
|
12372
|
+
v_signed := -v_qty;
|
|
12373
|
+
ELSE
|
|
12374
|
+
-- 'in' e 'reverse'. O estorno mant\xE9m a magnitude: o sign_check o dispensa,
|
|
12375
|
+
-- e quem decide o sentido dele \xE9 o movimento que ele desfaz.
|
|
12376
|
+
v_signed := v_qty;
|
|
12377
|
+
END IF;
|
|
12378
|
+
|
|
12379
|
+
SELECT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements WHERE id = p_id) INTO v_exists;
|
|
12380
|
+
IF v_exists THEN
|
|
12381
|
+
-- the ledger is append-only: a second offer of the same movement is a replay,
|
|
12382
|
+
-- not an update. Nothing to do, and saying "migrated" again is honest \u2014
|
|
12383
|
+
-- the destination already holds exactly this row.
|
|
12384
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');
|
|
12385
|
+
END IF;
|
|
12386
|
+
|
|
12387
|
+
-- 005's single-writer gate refuses any write "however privileged" unless this
|
|
12388
|
+
-- is set. The migration IS one of the internal writers \u2014 definer-only, fenced
|
|
12389
|
+
-- and audited \u2014 so it declares itself rather than being exempted, and the
|
|
12390
|
+
-- after-insert trigger derives the position from what it writes.
|
|
12391
|
+
PERFORM set_config('fayz.inventory_internal', 'on', true);
|
|
12392
|
+
INSERT INTO public.plg_inventory_stock_movements
|
|
12393
|
+
(id, tenant_id, product_id, kind, quantity, unit_cost, total_cost,
|
|
12394
|
+
source_location_id, destination_location_id, batch_number, expiration_date,
|
|
12395
|
+
document_number, reason, notes, movement_date, metadata)
|
|
12396
|
+
VALUES (p_id, p_tenant, (p_row ->> 'product_id')::uuid, v_kind,
|
|
12397
|
+
v_signed,
|
|
12398
|
+
(p_row ->> 'unit_cost')::numeric, (p_row ->> 'total_cost')::numeric,
|
|
12399
|
+
(p_row ->> 'source_location_id')::uuid, (p_row ->> 'destination_location_id')::uuid,
|
|
12400
|
+
p_row ->> 'batch_number', (p_row ->> 'expiration_date')::date,
|
|
12401
|
+
p_row ->> 'document_number', p_row ->> 'reason', p_row ->> 'notes',
|
|
12402
|
+
coalesce((p_row ->> 'movement_date')::timestamptz, now()), v_meta);
|
|
12403
|
+
PERFORM set_config('fayz.inventory_internal', 'off', true);
|
|
12404
|
+
|
|
12405
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_movements', p_id, jsonb_build_object('reason', 'created')); END IF;
|
|
12406
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');
|
|
12407
|
+
END $function$;
|
|
12408
|
+
|
|
12409
|
+
COMMENT ON FUNCTION migration.upsert_inv_movement(uuid, uuid, jsonb, jsonb) IS
|
|
12410
|
+
'Movimento de estoque do V1 para o V2. Recebe magnitude + tipo (a conven\xE7\xE3o do V1) e grava quantidade ASSINADA (a conven\xE7\xE3o da tabela). A dire\xE7\xE3o do ajuste vem de adjust_delta \u2014 ver 009.';
|
|
12411
|
+
|
|
12412
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12413
|
+
-- 2. O de-para abre a porta para a dire\xE7\xE3o do ajuste
|
|
12414
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12415
|
+
--
|
|
12416
|
+
-- \`adjust_delta\` n\xE3o \xE9 coluna do V1: \xE9 derivada pelo plano de leitura, a partir
|
|
12417
|
+
-- de \`stock_count_items\`. Entra no mapa como campo simples porque \xE9 isso que
|
|
12418
|
+
-- ela \xE9 no \`p_row\` \u2014 o writer l\xEA e n\xE3o grava, ent\xE3o ela n\xE3o precisa (nem pode)
|
|
12419
|
+
-- casar com coluna do destino.
|
|
12420
|
+
|
|
12421
|
+
SET LOCAL migration.allowlist_unlock = 'on';
|
|
12422
|
+
|
|
12423
|
+
UPDATE migration.allowlist
|
|
12424
|
+
SET columns = columns || jsonb_build_object('adjust_delta', 'adjust_delta')
|
|
12425
|
+
WHERE source_table = 'stock_movements';
|
|
12426
|
+
|
|
12427
|
+
RESET migration.allowlist_unlock;
|
|
12428
|
+
`;
|
|
12429
|
+
var MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA = `-- A entrada de notas atravessa.
|
|
12430
|
+
--
|
|
12431
|
+
-- A importa\xE7\xE3o de DANFE do V1 \u2014 607 notas (527 no Pertinho do C\xE9u, 80 no
|
|
12432
|
+
-- Espa\xE7o Facial), 3.008 linhas, 68 lotes \u2014 n\xE3o tinha nenhuma linha na
|
|
12433
|
+
-- \`migration.allowlist\`. Este arquivo escreve a dela e a das linhas; a parcela,
|
|
12434
|
+
-- que \xE9 do financeiro, mora em plugin-financial/084 (o financeiro roda ANTES do
|
|
12435
|
+
-- invent\xE1rio na corrente, ent\xE3o o de-para dela n\xE3o podia vir por aqui).
|
|
12436
|
+
--
|
|
12437
|
+
-- \u2500\u2500 O achado que mudou o desenho: a linha da nota J\xC1 \xE9 o movimento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12438
|
+
--
|
|
12439
|
+
-- O caminho \xF3bvio seria \`danfe_import_items\` \u2192 \`plg_inventory_stock_movements\`.
|
|
12440
|
+
-- Medido em 21/09/2026 no V1 de produ\xE7\xE3o, ele DOBRARIA o estoque:
|
|
12441
|
+
--
|
|
12442
|
+
-- tenant 8: 2.707 dos 2.860 itens (94,7%) j\xE1 t\xEAm um \`stock_movements\` no V1,
|
|
12443
|
+
-- e a tabela \`stock_movements\` do tenant 8 \xE9 INTEIRA de DANFE \u2014
|
|
12444
|
+
-- 2.707 de 2.707 linhas com \`danfe_import_item_id\`.
|
|
12445
|
+
-- ledger: 2.757 movimentos j\xE1 atravessaram (\`status = 'migrated'\`).
|
|
12446
|
+
--
|
|
12447
|
+
-- Quantidade e custo batem linha a linha (o movimento \xE9 gerado pela concilia\xE7\xE3o
|
|
12448
|
+
-- da nota). Criar um segundo movimento a partir do item somaria a mesma entrada
|
|
12449
|
+
-- duas vezes \u2014 e, pior, o gatilho \`plg_inventory_stock_movements_after_insert\`
|
|
12450
|
+
-- deriva a POSI\xC7\xC3O do movimento, ent\xE3o o saldo dobraria junto, sem ningu\xE9m
|
|
12451
|
+
-- acusar.
|
|
12452
|
+
--
|
|
12453
|
+
-- Ent\xE3o a linha da nota n\xE3o cria movimento: ela ADOTA o que j\xE1 atravessou.
|
|
12454
|
+
-- \`movement_uuid\` resolve pelo ledger de \`stock_movements\`, o writer devolve
|
|
12455
|
+
-- aquele id como \`target_id\`, e o conte\xFAdo da linha \u2014 o que o movimento n\xE3o
|
|
12456
|
+
-- carrega: NCM, CFOP, tributo, fator de convers\xE3o, produto sugerido pela IA \u2014
|
|
12457
|
+
-- vai para o \`result\` da opera\xE7\xE3o, que \xE9 onde o V2 j\xE1 guarda o que uma opera\xE7\xE3o
|
|
12458
|
+
-- de invent\xE1rio produziu (\`app.inventory_movement_json\`, \`inventory_receive\`).
|
|
12459
|
+
--
|
|
12460
|
+
-- Item sem movimento no V1 (153 no tenant 8, 114 no tenant 1) \xE9 linha que nunca
|
|
12461
|
+
-- virou estoque: entra no \`result\` e fica \`skipped\`. O porte n\xE3o inventa
|
|
12462
|
+
-- entrada que a origem n\xE3o fez.
|
|
12463
|
+
--
|
|
12464
|
+
-- \u2500\u2500 E o conte\xFAdo FISCAL da nota tamb\xE9m j\xE1 atravessa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12465
|
+
--
|
|
12466
|
+
-- 607 de 607 notas t\xEAm a contraparte em \`dfe_inbox\` pela chave de acesso, e as
|
|
12467
|
+
-- 607 t\xEAm o MESMO n\xFAmero de linhas em \`dfe_inbox_items\` (149 = 149 no tenant 1,
|
|
12468
|
+
-- 2.859 = 2.859 no tenant 8). O XML, o tributo por coluna e a duplicata j\xE1 v\xE3o
|
|
12469
|
+
-- para \`plg_fiscalbr_inbox_*\` por fiscal_br/003. Copiar tudo de novo aqui daria
|
|
12470
|
+
-- duas respostas para "qual o ICMS desta linha".
|
|
12471
|
+
--
|
|
12472
|
+
-- O que N\xC3O atravessava \xE9 a DECIS\xC3O DE ENTRADA feita sobre a nota: a qual
|
|
12473
|
+
-- produto a linha foi ligada, em que unidade de estoque, com que fator de
|
|
12474
|
+
-- convers\xE3o, em que conta cont\xE1bil, e o plano de pagamento que o operador
|
|
12475
|
+
-- montou. \xC9 isso que este arquivo traz.
|
|
12476
|
+
--
|
|
12477
|
+
-- \u2500\u2500 A nota \xE9 uma opera\xE7\xE3o de recebimento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12478
|
+
--
|
|
12479
|
+
-- \`plg_inventory_operations\` \xE9 o raz\xE3o de opera\xE7\xF5es do invent\xE1rio, com
|
|
12480
|
+
-- vocabul\xE1rio fechado (\`receive | transfer | adjust | reserve | confirm |
|
|
12481
|
+
-- reverse\`). Uma importa\xE7\xE3o de DANFE \xE9 um \`receive\`, e a \`idempotency_key\` dela
|
|
12482
|
+
-- \xE9 a CHAVE DE ACESSO: 607 de 607 notas t\xEAm chave, e ela \xE9 \xFAnica dentro do
|
|
12483
|
+
-- tenant nas duas casas. Isso faz o \`op.idempotency_key = doc.access_key\`
|
|
12484
|
+
-- ligar a opera\xE7\xE3o ao documento fiscal sem coluna nova e sem FK entre plugins
|
|
12485
|
+
-- (o fiscal_br roda DEPOIS do invent\xE1rio).
|
|
12486
|
+
--
|
|
12487
|
+
-- \u2500\u2500 A coluna que eu tive de criar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12488
|
+
--
|
|
12489
|
+
-- \`plg_inventory_operations\` n\xE3o tinha \`metadata\`. Sem ela o \`by_shape\` joga
|
|
12490
|
+
-- fora EM SIL\xCANCIO tudo que n\xE3o casa com uma coluna do destino \u2014 que aqui s\xE3o
|
|
12491
|
+
-- os 20 campos da nota \u2014 e joga fora tamb\xE9m a proveni\xEAncia que
|
|
12492
|
+
-- \`_upsert_row_impl\` carimba em toda linha portada (\`migration.source_table\`,
|
|
12493
|
+
-- \`source_numeric_id\`, \`batch_id\`). Seriam 607 notas no V2 sem como dizer de
|
|
12494
|
+
-- onde vieram. \xC9 a \xFAnica coluna nova deste arquivo.
|
|
12495
|
+
--
|
|
12496
|
+
-- Idempotente.
|
|
12497
|
+
|
|
12498
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12499
|
+
-- 1. A opera\xE7\xE3o ganha onde guardar a nota
|
|
12500
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12501
|
+
|
|
12502
|
+
ALTER TABLE public.plg_inventory_operations
|
|
12503
|
+
ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;
|
|
12504
|
+
|
|
12505
|
+
COMMENT ON COLUMN public.plg_inventory_operations.metadata IS
|
|
12506
|
+
'O que a opera\xE7\xE3o carrega e nenhuma coluna nomeia. No porte \xE9 a nota de '
|
|
12507
|
+
'entrada inteira (metadata.danfe) e a proveni\xEAncia da linha (metadata.migration). '
|
|
12508
|
+
'A identidade da nota fica em idempotency_key, que \xE9 a chave de acesso.';
|
|
12509
|
+
|
|
12510
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12511
|
+
-- 2. O writer gen\xE9rico com a porteira do invent\xE1rio aberta
|
|
12512
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12513
|
+
--
|
|
12514
|
+
-- \`migration.upsert_by_shape\` (spine 123/124) resolve o upsert desta tabela
|
|
12515
|
+
-- sozinho: as colunas do destino j\xE1 t\xEAm o nome que o de-para produz. O que ele
|
|
12516
|
+
-- N\xC3O faz \xE9 abrir a porteira \`fayz.inventory_internal\`, e as tr\xEAs tabelas de
|
|
12517
|
+
-- invent\xE1rio recusam qualquer escrita sem ela \u2014 inclusive a do porte, que \xE9
|
|
12518
|
+
-- definer e cercada:
|
|
12519
|
+
--
|
|
12520
|
+
-- inventory: plg_inventory_operations is written only through the inventory RPCs
|
|
12521
|
+
--
|
|
12522
|
+
-- Foi essa frase que p\xF4s 339 posi\xE7\xF5es em quarentena na 004 e que a 005 acabou
|
|
12523
|
+
-- dando por certa. Ela continua certa para a POSI\xC7\xC3O \u2014 a 005 tirou
|
|
12524
|
+
-- \`stock_positions\` da allowlist porque o saldo \xE9 consequ\xEAncia do movimento \u2014
|
|
12525
|
+
-- e n\xE3o diz nada contra a OPERA\xC7\xC3O, que \xE9 um fato do V1 e n\xE3o um derivado de
|
|
12526
|
+
-- nada. O que ela exige \xE9 que quem escreve se declare.
|
|
12527
|
+
--
|
|
12528
|
+
-- Em vez de copiar o by_shape inteiro s\xF3 para acrescentar duas linhas, este
|
|
12529
|
+
-- writer abre a porteira, delega, e fecha. A porteira \xE9 transacional
|
|
12530
|
+
-- (\`set_config(..., true)\`): ela morre com a transa\xE7\xE3o e n\xE3o vaza para a linha
|
|
12531
|
+
-- seguinte do lote.
|
|
12532
|
+
|
|
12533
|
+
CREATE OR REPLACE FUNCTION migration.upsert_inv_shape(
|
|
12534
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
12535
|
+
) RETURNS jsonb
|
|
12536
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
12537
|
+
AS $function$
|
|
12538
|
+
DECLARE
|
|
12539
|
+
v_prev text := coalesce(current_setting('fayz.inventory_internal', true), 'off');
|
|
12540
|
+
v_res jsonb;
|
|
12541
|
+
BEGIN
|
|
12542
|
+
PERFORM set_config('fayz.inventory_internal', 'on', true);
|
|
12543
|
+
v_res := migration.upsert_by_shape(p_tenant, p_id, p_row, p_args);
|
|
12544
|
+
PERFORM set_config('fayz.inventory_internal', v_prev, true);
|
|
12545
|
+
RETURN v_res;
|
|
12546
|
+
END $function$;
|
|
12547
|
+
|
|
12548
|
+
COMMENT ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) IS
|
|
12549
|
+
'upsert_by_shape com fayz.inventory_internal ligado. Para a tabela de invent\xE1rio '
|
|
12550
|
+
'que j\xE1 tem a forma certa e cuja porteira de escrita \xFAnica recusaria o porte.';
|
|
12551
|
+
|
|
12552
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12553
|
+
-- 3. O writer da linha da nota \u2014 adota, nunca duplica
|
|
12554
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12555
|
+
--
|
|
12556
|
+
-- Serve as duas partes da nota, escolhidas por \`writer_args.part\`:
|
|
12557
|
+
--
|
|
12558
|
+
-- 'item' a linha da nota \u2192 o MOVIMENTO que ela gerou no V1
|
|
12559
|
+
-- 'batch' o lote da linha \u2192 a POSI\xC7\xC3O que aquele movimento derivou
|
|
12560
|
+
--
|
|
12561
|
+
-- Nenhuma das duas insere: as duas devolvem o id do que j\xE1 est\xE1 l\xE1. \xC9 a \xFAnica
|
|
12562
|
+
-- forma de a linha atravessar sem somar estoque duas vezes, porque
|
|
12563
|
+
-- \`plg_inventory_stock_movements\` \xE9 append-only (o gatilho recusa at\xE9 um UPDATE
|
|
12564
|
+
-- que s\xF3 acrescente coluna nula) e a posi\xE7\xE3o \xE9 consequ\xEAncia do movimento.
|
|
12565
|
+
--
|
|
12566
|
+
-- O conte\xFAdo da linha vai para \`plg_inventory_operations.result\`, indexado pelo
|
|
12567
|
+
-- id da linha \u2014 objeto e n\xE3o lista, para a segunda oferta da mesma linha
|
|
12568
|
+
-- substituir a primeira em vez de empilhar.
|
|
12569
|
+
|
|
12570
|
+
CREATE OR REPLACE FUNCTION migration.upsert_inv_danfe_line(
|
|
12571
|
+
p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
|
|
12572
|
+
) RETURNS jsonb
|
|
12573
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
|
|
12574
|
+
AS $function$
|
|
12575
|
+
DECLARE
|
|
12576
|
+
v_part text := coalesce(p_args ->> 'part', 'item');
|
|
12577
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
12578
|
+
v_operation uuid := (p_row ->> 'operation_id')::uuid;
|
|
12579
|
+
v_movement uuid := (p_row ->> 'movement_id')::uuid;
|
|
12580
|
+
v_product uuid := (p_row ->> 'product_id')::uuid;
|
|
12581
|
+
v_location uuid := (p_row ->> 'stock_location_id')::uuid;
|
|
12582
|
+
v_target uuid;
|
|
12583
|
+
v_linha jsonb;
|
|
12584
|
+
v_prev text;
|
|
12585
|
+
v_standalone boolean := migration._standalone();
|
|
12586
|
+
BEGIN
|
|
12587
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
12588
|
+
|
|
12589
|
+
IF p_id IS NULL THEN
|
|
12590
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
12591
|
+
'reason', 'uma linha de nota precisa de id');
|
|
12592
|
+
END IF;
|
|
12593
|
+
IF v_operation IS NULL THEN
|
|
12594
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
12595
|
+
'reason', 'linha de nota cuja importa\xE7\xE3o ainda n\xE3o atravessou');
|
|
12596
|
+
END IF;
|
|
12597
|
+
IF NOT EXISTS (SELECT 1 FROM public.plg_inventory_operations o
|
|
12598
|
+
WHERE o.id = v_operation AND o.tenant_id = p_tenant) THEN
|
|
12599
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
|
|
12600
|
+
'reason', format('a opera\xE7\xE3o %s n\xE3o existe neste tenant', v_operation));
|
|
12601
|
+
END IF;
|
|
12602
|
+
|
|
12603
|
+
-- O que atravessa da linha: tudo que o movimento n\xE3o nomeia. Fica no \`result\`
|
|
12604
|
+
-- da opera\xE7\xE3o, que \xE9 onde o V2 guarda o que uma opera\xE7\xE3o produziu.
|
|
12605
|
+
-- \`coalesce\` porque \`NULL || jsonb\` \xE9 NULL, e uma nota sem nenhum campo de
|
|
12606
|
+
-- linha mapeado apagaria o registro inteiro em sil\xEAncio.
|
|
12607
|
+
v_linha := jsonb_strip_nulls(
|
|
12608
|
+
coalesce(v_meta -> 'danfe' -> CASE WHEN v_part = 'batch' THEN 'batch' ELSE 'line' END, '{}'::jsonb)
|
|
12609
|
+
|| jsonb_build_object(
|
|
12610
|
+
'movement_id', to_jsonb(v_movement),
|
|
12611
|
+
'product_id', to_jsonb(v_product),
|
|
12612
|
+
'line_no', p_row -> 'line_no',
|
|
12613
|
+
'quantity', p_row -> 'quantity',
|
|
12614
|
+
'batch_number', p_row -> 'batch_number',
|
|
12615
|
+
'expiration_date', p_row -> 'expiration_date'));
|
|
12616
|
+
|
|
12617
|
+
IF v_part = 'batch' THEN
|
|
12618
|
+
-- O lote n\xE3o cria posi\xE7\xE3o: ele aponta para a que o movimento derivou. A
|
|
12619
|
+
-- chave da posi\xE7\xE3o \xE9 (tenant, produto, local, lote, validade) \u2014 a mesma que
|
|
12620
|
+
-- \`app.inventory_apply_to_position\` usa para decidir se soma ou cria.
|
|
12621
|
+
IF v_movement IS NOT NULL AND v_product IS NOT NULL AND v_location IS NOT NULL THEN
|
|
12622
|
+
SELECT sp.id INTO v_target
|
|
12623
|
+
FROM public.plg_inventory_stock_positions sp
|
|
12624
|
+
WHERE sp.tenant_id = p_tenant
|
|
12625
|
+
AND sp.product_id = v_product
|
|
12626
|
+
AND sp.stock_location_id = v_location
|
|
12627
|
+
AND sp.batch_number IS NOT DISTINCT FROM nullif(btrim(p_row ->> 'batch_number'), '')
|
|
12628
|
+
AND sp.expiration_date IS NOT DISTINCT FROM (p_row ->> 'expiration_date')::date
|
|
12629
|
+
LIMIT 1;
|
|
12630
|
+
END IF;
|
|
12631
|
+
ELSE
|
|
12632
|
+
v_target := v_movement;
|
|
12633
|
+
END IF;
|
|
12634
|
+
|
|
12635
|
+
-- A porteira do invent\xE1rio: a opera\xE7\xE3o recusa UPDATE sem ela, do porte
|
|
12636
|
+
-- inclusive. Transacional, para n\xE3o vazar para a linha seguinte.
|
|
12637
|
+
v_prev := coalesce(current_setting('fayz.inventory_internal', true), 'off');
|
|
12638
|
+
PERFORM set_config('fayz.inventory_internal', 'on', true);
|
|
12639
|
+
UPDATE public.plg_inventory_operations o
|
|
12640
|
+
SET result = coalesce(o.result, '{}'::jsonb) || jsonb_build_object(
|
|
12641
|
+
CASE WHEN v_part = 'batch' THEN 'batches' ELSE 'lines' END,
|
|
12642
|
+
coalesce(o.result -> CASE WHEN v_part = 'batch' THEN 'batches' ELSE 'lines' END, '{}'::jsonb)
|
|
12643
|
+
|| jsonb_build_object(p_id::text, v_linha))
|
|
12644
|
+
WHERE o.id = v_operation AND o.tenant_id = p_tenant;
|
|
12645
|
+
PERFORM set_config('fayz.inventory_internal', v_prev, true);
|
|
12646
|
+
|
|
12647
|
+
IF v_target IS NULL THEN
|
|
12648
|
+
-- Linha declarada e nunca conciliada: no V1 ela n\xE3o virou estoque, e o
|
|
12649
|
+
-- porte n\xE3o inventa a entrada que a origem n\xE3o fez. O conte\xFAdo dela j\xE1 foi
|
|
12650
|
+
-- gravado no \`result\` acima \u2014 o que falta \xE9 s\xF3 o v\xEDnculo com o raz\xE3o.
|
|
12651
|
+
RETURN jsonb_build_object('status', 'skipped',
|
|
12652
|
+
'reason', CASE WHEN v_part = 'batch'
|
|
12653
|
+
THEN 'lote sem posi\xE7\xE3o no V2 \u2014 o movimento dele n\xE3o atravessou'
|
|
12654
|
+
ELSE 'linha sem movimento no V1 \u2014 nunca virou estoque' END);
|
|
12655
|
+
END IF;
|
|
12656
|
+
|
|
12657
|
+
IF v_standalone THEN
|
|
12658
|
+
PERFORM migration._audit(p_tenant,
|
|
12659
|
+
CASE WHEN v_part = 'batch' THEN 'public.plg_inventory_stock_positions'
|
|
12660
|
+
ELSE 'public.plg_inventory_stock_movements' END,
|
|
12661
|
+
v_target, jsonb_build_object('reason', 'adopted'));
|
|
12662
|
+
END IF;
|
|
12663
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', v_target,
|
|
12664
|
+
'reason', CASE WHEN v_part = 'batch'
|
|
12665
|
+
THEN 'a posi\xE7\xE3o deste lote j\xE1 \xE9 consequ\xEAncia do movimento'
|
|
12666
|
+
ELSE 'a linha \xE9 o movimento que j\xE1 atravessou' END);
|
|
12667
|
+
END $function$;
|
|
12668
|
+
|
|
12669
|
+
COMMENT ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) IS
|
|
12670
|
+
'A linha (ou o lote) da nota de entrada. Adota o movimento/posi\xE7\xE3o que o V1 j\xE1 '
|
|
12671
|
+
'gerou em vez de criar um segundo \u2014 94,7% das linhas do tenant 8 t\xEAm movimento, '
|
|
12672
|
+
'e duplic\xE1-lo dobraria o estoque \u2014 e guarda o conte\xFAdo da linha em operations.result.';
|
|
12673
|
+
|
|
12674
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12675
|
+
-- 4. O vocabul\xE1rio de writers aceita os nomes novos
|
|
12676
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12677
|
+
--
|
|
12678
|
+
-- Lendo e costurando o CHECK que est\xE1 l\xE1, nunca reescrevendo com lista literal:
|
|
12679
|
+
-- a lista literal apaga as palavras que os outros passos da corrente
|
|
12680
|
+
-- acrescentaram, e o erro s\xF3 aparece na onda seguinte.
|
|
12681
|
+
|
|
12682
|
+
DO $$
|
|
12683
|
+
DECLARE v_def text;
|
|
12684
|
+
BEGIN
|
|
12685
|
+
SELECT pg_get_constraintdef(oid) INTO v_def
|
|
12686
|
+
FROM pg_constraint WHERE conname = 'allowlist_writer_check';
|
|
12687
|
+
IF v_def IS NULL OR strpos(v_def, 'inv_shape') > 0 THEN RETURN; END IF;
|
|
12688
|
+
ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
|
|
12689
|
+
EXECUTE replace(
|
|
12690
|
+
'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
|
|
12691
|
+
'''person''::text', '''person''::text, ''inv_shape''::text');
|
|
12692
|
+
END $$;
|
|
12693
|
+
|
|
12694
|
+
DO $$
|
|
12695
|
+
DECLARE v_def text;
|
|
12696
|
+
BEGIN
|
|
12697
|
+
SELECT pg_get_constraintdef(oid) INTO v_def
|
|
12698
|
+
FROM pg_constraint WHERE conname = 'allowlist_writer_check';
|
|
12699
|
+
IF v_def IS NULL OR strpos(v_def, 'inv_danfe_line') > 0 THEN RETURN; END IF;
|
|
12700
|
+
ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
|
|
12701
|
+
EXECUTE replace(
|
|
12702
|
+
'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
|
|
12703
|
+
'''person''::text', '''person''::text, ''inv_danfe_line''::text');
|
|
12704
|
+
END $$;
|
|
12705
|
+
|
|
12706
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12707
|
+
-- 5. O de-para
|
|
12708
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12709
|
+
--
|
|
12710
|
+
-- A ordem \xE9 a da depend\xEAncia: a nota antes das linhas, as linhas antes dos
|
|
12711
|
+
-- lotes. As tr\xEAs resolvem \`import_id@danfe_imports\` pelo ledger, e o ledger s\xF3
|
|
12712
|
+
-- responde depois que a nota atravessou.
|
|
12713
|
+
|
|
12714
|
+
SET LOCAL migration.allowlist_unlock = 'on';
|
|
12715
|
+
|
|
12716
|
+
SELECT migration._allow(
|
|
12717
|
+
'danfe_imports', 'public.plg_inventory_operations', 'inventory', 'inv_shape',
|
|
12718
|
+
jsonb_build_object('target', 'public.plg_inventory_operations'),
|
|
12719
|
+
NULL::text[],
|
|
12720
|
+
jsonb_build_object(
|
|
12721
|
+
'uuid', 'id',
|
|
12722
|
+
-- \`kind\` e \`op_status\` v\xEAm do augment do extrator: o V1 n\xE3o guarda a
|
|
12723
|
+
-- palavra "receive" porque a tabela INTEIRA \xE9 de entrada, e o status dele
|
|
12724
|
+
-- tem cinco palavras (pending | partial | stocked | conciliated |
|
|
12725
|
+
-- asset_registered) onde o V2 tem duas. Medido: 71 pendentes e 536
|
|
12726
|
+
-- conclu\xEDdas nos dois tenants.
|
|
12727
|
+
'kind', 'kind',
|
|
12728
|
+
'op_status', 'status',
|
|
12729
|
+
'access_key', 'idempotency_key',
|
|
12730
|
+
'unit_id', 'unit_id@companies',
|
|
12731
|
+
'created_at', 'created_at',
|
|
12732
|
+
'updated_at', 'updated_at',
|
|
12733
|
+
'tenant_id', '-',
|
|
12734
|
+
'status', 'jsonb:danfe.v1_status',
|
|
12735
|
+
'nfe_number', 'jsonb:danfe.nfe_number',
|
|
12736
|
+
'nfe_series', 'jsonb:danfe.nfe_series',
|
|
12737
|
+
'issue_date', 'jsonb:danfe.issue_date',
|
|
12738
|
+
'nature_operation', 'jsonb:danfe.nature_operation',
|
|
12739
|
+
'supplier_name', 'jsonb:danfe.supplier.name',
|
|
12740
|
+
'supplier_cnpj', 'jsonb:danfe.supplier.cnpj',
|
|
12741
|
+
'supplier_id', 'jsonb:danfe.supplier.v1_ref',
|
|
12742
|
+
'total_value', 'jsonb:danfe.totals.total',
|
|
12743
|
+
'discount_value', 'jsonb:danfe.totals.discount',
|
|
12744
|
+
'freight_value', 'jsonb:danfe.totals.freight',
|
|
12745
|
+
'insurance_value', 'jsonb:danfe.totals.insurance',
|
|
12746
|
+
'other_expenses', 'jsonb:danfe.totals.other',
|
|
12747
|
+
'total_items', 'jsonb:danfe.total_items',
|
|
12748
|
+
'conciliated_items', 'jsonb:danfe.conciliated_items',
|
|
12749
|
+
'imported_by', 'jsonb:danfe.imported_by',
|
|
12750
|
+
'payment_summary', 'jsonb:danfe.payment_summary',
|
|
12751
|
+
'default_chart_account_id', 'jsonb:danfe.v1_default_chart_account_ref',
|
|
12752
|
+
'id', 'jsonb:danfe.v1_ref'),
|
|
12753
|
+
'A importa\xE7\xE3o de DANFE \xE9 uma opera\xE7\xE3o de RECEBIMENTO, e a idempotency_key '
|
|
12754
|
+
'dela \xE9 a chave de acesso \u2014 607 de 607 notas t\xEAm chave e ela \xE9 \xFAnica no '
|
|
12755
|
+
'tenant, ent\xE3o op.idempotency_key = doc.access_key liga a opera\xE7\xE3o ao '
|
|
12756
|
+
'documento fiscal sem FK entre plugins. O conte\xFAdo fiscal n\xE3o \xE9 copiado: as '
|
|
12757
|
+
'607 notas j\xE1 est\xE3o em plg_fiscalbr_inbox_documents, com o mesmo n\xFAmero de '
|
|
12758
|
+
'linhas em 607 de 607.'
|
|
12759
|
+
);
|
|
12760
|
+
|
|
12761
|
+
SELECT migration._allow(
|
|
12762
|
+
'danfe_import_items', 'public.plg_inventory_stock_movements', 'inventory', 'inv_danfe_line',
|
|
12763
|
+
jsonb_build_object('part', 'item'),
|
|
12764
|
+
NULL::text[],
|
|
12765
|
+
jsonb_build_object(
|
|
12766
|
+
'uuid', 'id',
|
|
12767
|
+
'import_id', 'operation_id@danfe_imports',
|
|
12768
|
+
-- O movimento que ESTA linha gerou no V1. Resolve pelo ledger de
|
|
12769
|
+
-- \`stock_movements\`; nulo quando a linha nunca foi conciliada, e
|
|
12770
|
+
-- irresolvido (quarentena \`fk\`) quando o movimento existe e n\xE3o atravessou.
|
|
12771
|
+
'movement_uuid', 'movement_id@stock_movements',
|
|
12772
|
+
'linked_product_id', 'product_id@products',
|
|
12773
|
+
'item_number', 'line_no',
|
|
12774
|
+
'created_at', 'created_at',
|
|
12775
|
+
'tenant_id', '-',
|
|
12776
|
+
'description', 'jsonb:danfe.line.description',
|
|
12777
|
+
'product_code_supplier', 'jsonb:danfe.line.supplier_code',
|
|
12778
|
+
'ean_barcode', 'jsonb:danfe.line.ean',
|
|
12779
|
+
'ncm', 'jsonb:danfe.line.ncm',
|
|
12780
|
+
'cfop', 'jsonb:danfe.line.cfop',
|
|
12781
|
+
'quantity', 'jsonb:danfe.line.quantity',
|
|
12782
|
+
'unit_abbreviation', 'jsonb:danfe.line.unit',
|
|
12783
|
+
'unit_price', 'jsonb:danfe.line.unit_price',
|
|
12784
|
+
'total_price', 'jsonb:danfe.line.total_price',
|
|
12785
|
+
'icms_value', 'jsonb:danfe.line.icms_value',
|
|
12786
|
+
'ipi_value', 'jsonb:danfe.line.ipi_value',
|
|
12787
|
+
'pis_value', 'jsonb:danfe.line.pis_value',
|
|
12788
|
+
'cofins_value', 'jsonb:danfe.line.cofins_value',
|
|
12789
|
+
'entry_type', 'jsonb:danfe.line.entry_type',
|
|
12790
|
+
'cst_entry', 'jsonb:danfe.line.cst_entry',
|
|
12791
|
+
'chart_account_id', 'jsonb:danfe.line.v1_chart_account_ref',
|
|
12792
|
+
'stock_unit_id', 'jsonb:danfe.line.v1_stock_unit_ref',
|
|
12793
|
+
'stock_quantity', 'jsonb:danfe.line.stock_quantity',
|
|
12794
|
+
'conversion_factor', 'jsonb:danfe.line.conversion_factor',
|
|
12795
|
+
'cost_mode', 'jsonb:danfe.line.cost_mode',
|
|
12796
|
+
'reconciled', 'jsonb:danfe.line.reconciled',
|
|
12797
|
+
'xml_batches', 'jsonb:danfe.line.xml_batches',
|
|
12798
|
+
'ai_suggested', 'jsonb:danfe.line.ai.suggested',
|
|
12799
|
+
'ai_create_product', 'jsonb:danfe.line.ai.create_product',
|
|
12800
|
+
'ai_product_name', 'jsonb:danfe.line.ai.product_name',
|
|
12801
|
+
'ai_product_barcode', 'jsonb:danfe.line.ai.product_barcode',
|
|
12802
|
+
'ai_product_cost_price', 'jsonb:danfe.line.ai.product_cost_price',
|
|
12803
|
+
'ai_product_unit_id', 'jsonb:danfe.line.ai.v1_product_unit_ref',
|
|
12804
|
+
'id', 'jsonb:danfe.line.v1_ref'),
|
|
12805
|
+
'A linha da nota. N\xC3O cria movimento: 2.707 dos 2.860 itens do tenant 8 j\xE1 '
|
|
12806
|
+
't\xEAm o movimento que o V1 gerou, e ele j\xE1 atravessou \u2014 um segundo dobraria a '
|
|
12807
|
+
'entrada e, pelo gatilho after_insert, o saldo junto. A linha adota aquele '
|
|
12808
|
+
'movimento e guarda em operations.result o que ele n\xE3o nomeia: fator de '
|
|
12809
|
+
'convers\xE3o, conta cont\xE1bil, CST de entrada e o produto sugerido pela IA.'
|
|
12810
|
+
);
|
|
12811
|
+
|
|
12812
|
+
SELECT migration._allow(
|
|
12813
|
+
'danfe_import_item_batches', 'public.plg_inventory_stock_positions', 'inventory', 'inv_danfe_line',
|
|
12814
|
+
jsonb_build_object('part', 'batch'),
|
|
12815
|
+
NULL::text[],
|
|
12816
|
+
jsonb_build_object(
|
|
12817
|
+
'uuid', 'id',
|
|
12818
|
+
'import_id', 'operation_id@danfe_imports',
|
|
12819
|
+
'product_ref', 'product_id@products',
|
|
12820
|
+
'stock_location_id', 'stock_location_id@stock_locations',
|
|
12821
|
+
'stock_movement_uuid', 'movement_id@stock_movements',
|
|
12822
|
+
'quantity', 'quantity',
|
|
12823
|
+
'batch_number', 'batch_number',
|
|
12824
|
+
'expiration_date', 'expiration_date',
|
|
12825
|
+
'created_at', 'created_at',
|
|
12826
|
+
'updated_at', 'updated_at',
|
|
12827
|
+
'tenant_id', '-',
|
|
12828
|
+
'item_id', 'jsonb:danfe.batch.v1_item_ref',
|
|
12829
|
+
'received_at', 'jsonb:danfe.batch.received_at',
|
|
12830
|
+
'received_by', 'jsonb:danfe.batch.v1_received_by',
|
|
12831
|
+
'source', 'jsonb:danfe.batch.source',
|
|
12832
|
+
'id', 'jsonb:danfe.batch.v1_ref'),
|
|
12833
|
+
'O lote declarado na entrada \u2014 66 no tenant 1 (todos do XML) e 2 no tenant 8 '
|
|
12834
|
+
'(manuais). N\xE3o desfaz a 005: a posi\xE7\xE3o continua sendo consequ\xEAncia do '
|
|
12835
|
+
'movimento e este de-para n\xE3o grava nenhuma. O gatilho after_insert j\xE1 criou '
|
|
12836
|
+
'a posi\xE7\xE3o com este lote e esta validade, e a linha do lote aponta para ela. '
|
|
12837
|
+
'32 dos 68 lotes t\xEAm movimento; os outros 36 ficam skipped, porque lote sem '
|
|
12838
|
+
'movimento nunca foi saldo.'
|
|
12839
|
+
);
|
|
12840
|
+
|
|
12841
|
+
DELETE FROM migration.excluded_tables
|
|
12842
|
+
WHERE source_table IN ('danfe_imports', 'danfe_import_items', 'danfe_import_item_batches');
|
|
12843
|
+
|
|
12844
|
+
RESET migration.allowlist_unlock;
|
|
12845
|
+
|
|
12846
|
+
|
|
12847
|
+
-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12848
|
+
--
|
|
12849
|
+
-- Fun\xE7\xE3o nova nasce execut\xE1vel por PUBLIC, e \`anon\`/\`authenticated\` herdam
|
|
12850
|
+
-- dali. Estas s\xE3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer
|
|
12851
|
+
-- inquilino, e uma delas alcan\xE7\xE1vel pela chave an\xF4nima \xE9 a base inteira
|
|
12852
|
+
-- alcan\xE7\xE1vel pela chave an\xF4nima. S\xF3 o service_role, que \xE9 quem roda a onda.
|
|
12853
|
+
REVOKE ALL ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
|
|
12854
|
+
GRANT EXECUTE ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
12855
|
+
REVOKE ALL ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
|
|
12856
|
+
GRANT EXECUTE ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
12857
|
+
`;
|
|
12858
|
+
var MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS = `-- 011_o_consumo_tem_duas_origens.sql
|
|
12859
|
+
--
|
|
12860
|
+
-- A AUDITORIA DE USO ESTAVA VAZIA, E A CULPA N\xC3O ERA DELA.
|
|
12861
|
+
--
|
|
12862
|
+
-- A 001 leu o consumo no \xFAnico lugar onde ele existia quando foi escrita: o
|
|
12863
|
+
-- raz\xE3o, \`plg_inventory_stock_movements\` com \`source_item_type='order_item'\`.
|
|
12864
|
+
-- Isso pressup\xF5e que consumir DEU BAIXA. \xC9 verdade para o consumo que nasce no
|
|
12865
|
+
-- V2 \u2014 \`order.completed\` consome a receita no mesmo instante \u2014 e \xE9 falso para
|
|
12866
|
+
-- tudo o que veio do V1.
|
|
12867
|
+
--
|
|
12868
|
+
-- O N\xDAMERO QUE OBRIGA ESTA MIGRATION. No V1, tenant 1:
|
|
12869
|
+
--
|
|
12870
|
+
-- execution_products ......................... 117.024 linhas
|
|
12871
|
+
-- stock_deducted = true ...................... 0
|
|
12872
|
+
-- stock_movement_id preenchido ............... 0
|
|
12873
|
+
--
|
|
12874
|
+
-- Zero. Em nenhum momento da hist\xF3ria daquele banco o consumo declarado virou
|
|
12875
|
+
-- movimento de estoque \u2014 \xE9 exatamente o que os dois FLOW.md descrevem: no V1 a
|
|
12876
|
+
-- auditoria \xE9 um PORT\xC3O PR\xC9-BAIXA, e a fila nunca foi drenada. Migrado, esse
|
|
12877
|
+
-- hist\xF3rico chegou ao V2 como \`plg_clinical_session_products\` (116.911 linhas
|
|
12878
|
+
-- no Espa\xE7o Facial) e n\xE3o produziu nenhum movimento, porque n\xE3o havia nenhum
|
|
12879
|
+
-- para produzir.
|
|
12880
|
+
--
|
|
12881
|
+
-- Resultado medido no cluster, pelo papel \`authenticated\` com as claims do
|
|
12882
|
+
-- tenant: \`v_inventory_consumption_audit\` devolvia **0 linhas**. A tela do V2 \xE9 um
|
|
12883
|
+
-- porte fiel da do V1 \u2014 produto, quantidade com o \xABPadr\xE3o: N\xBB s\xF3 quando
|
|
12884
|
+
-- diverge, \xABSem posi\xE7\xE3o\xBB, data, profissional, cliente \u2014 e estava passando
|
|
12885
|
+
-- fome.
|
|
12886
|
+
--
|
|
12887
|
+
-- O QUE MUDA AQUI: o consumo passa a ter DUAS ORIGENS.
|
|
12888
|
+
--
|
|
12889
|
+
-- \`ledger\` \u2014 a baixa que realmente saiu do estoque (a 001, intacta).
|
|
12890
|
+
-- \`clinical\` \u2014 a declara\xE7\xE3o do atendimento que nunca virou baixa.
|
|
12891
|
+
--
|
|
12892
|
+
-- A PERGUNTA DA TELA \xC9 A MESMA NAS DUAS. \xABO que o profissional disse ter usado
|
|
12893
|
+
-- bate com o padr\xE3o?\xBB n\xE3o depende de o saldo ter mudado. O que muda \xE9 o que
|
|
12894
|
+
-- \xABcorrigir\xBB significa, e \xE9 por isso que a vista ganha \`stock_posted\`: na
|
|
12895
|
+
-- origem do raz\xE3o corrigir DEVOLVE ao saldo; na origem cl\xEDnica n\xE3o h\xE1 o que
|
|
12896
|
+
-- devolver, e corrigir \xE9 o mesmo ato do V1 \u2014 conserta a quantidade declarada e
|
|
12897
|
+
-- aprova.
|
|
12898
|
+
--
|
|
12899
|
+
-- POR QUE N\xC3O UMA FILA NOVA. Pelo mesmo motivo da 001: seria um segundo lugar
|
|
12900
|
+
-- guardando a mesma quantidade. \`plg_clinical_session_products\` J\xC1 \xE9 o registro
|
|
12901
|
+
-- do consumo declarado, com \`default_quantity\` (o \xABPadr\xE3o: N\xBB do V1),
|
|
12902
|
+
-- \`used_quantity\`, \`is_audited\`, \`audited_at\` e \`audit_notes\`. A auditoria l\xEA
|
|
12903
|
+
-- de onde o dado mora.
|
|
12904
|
+
--
|
|
12905
|
+
-- FORMA DA VISTA: as 29 colunas existentes continuam nas mesmas posi\xE7\xF5es e com
|
|
12906
|
+
-- os MESMOS tipos \u2014 \`variance\` \xE9 \`numeric\` sem typmod, e um \`numeric(14,4)\` ali
|
|
12907
|
+
-- faria o \`CREATE OR REPLACE\` recusar. As duas colunas novas entram no fim, que
|
|
12908
|
+
-- \xE9 o \xFAnico lugar onde o REPLACE aceita coluna nova. Nada de DROP: consultado o
|
|
12909
|
+
-- \`pg_depend\`, nada depende desta vista hoje, e mesmo assim um DROP CASCADE
|
|
12910
|
+
-- aqui seria o quarto acidente da casa.
|
|
12911
|
+
--
|
|
12912
|
+
-- \`security_invoker\` PRESERVADO. Sem ele a vista leria com os direitos de quem
|
|
12913
|
+
-- a criou e furaria a RLS das duas origens.
|
|
12914
|
+
--
|
|
12915
|
+
-- CONSEQU\xCANCIA CONHECIDA, e ela \xE9 de prop\xF3sito: com \`security_invoker\`, a
|
|
12916
|
+
-- origem cl\xEDnica \xE9 filtrada pela policy de \`plg_clinical_session_products\`, que
|
|
12917
|
+
-- exige \`clinical.read\`. Um estoquista com \`inventory.read\` e sem
|
|
12918
|
+
-- \`clinical.read\` continua vendo a auditoria vazia. Alargar isso \xE9 decis\xE3o da
|
|
12919
|
+
-- policy do plugin cl\xEDnico \u2014 dono da tabela \u2014, n\xE3o de uma migration do estoque,
|
|
12920
|
+
-- e escond\xEA-la atr\xE1s de um \`security_definer\` seria trocar uma tela vazia por
|
|
12921
|
+
-- um vazamento.
|
|
12922
|
+
|
|
12923
|
+
-- \u2500\u2500 replay-safety \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
12924
|
+
--
|
|
12925
|
+
-- DROP before CREATE, and the reason is the rule this file exists under: the
|
|
12926
|
+
-- chain bench re-applies every migration on a populated database, and
|
|
12927
|
+
-- \`CREATE OR REPLACE VIEW\` may only APPEND columns \u2014 it refuses a narrower
|
|
12928
|
+
-- column list with "cannot drop columns from view". Files 012 through 018
|
|
12929
|
+
-- each widen this view, so on the second pass this statement would meet the
|
|
12930
|
+
-- final, wider shape and fail.
|
|
12931
|
+
--
|
|
12932
|
+
-- Dropping is free here and nowhere else: this view is born in this file. Its
|
|
12933
|
+
-- predecessor \`v_inventory_usage_audit\` (001, applied 2026-09-06) keeps its own
|
|
12934
|
+
-- name and its own narrow contract precisely so that it never has to be
|
|
12935
|
+
-- touched \u2014 an applied file with a changed checksum is a hard stop for
|
|
12936
|
+
-- \`fayz db apply\` on every cluster that already ran it.
|
|
12937
|
+
DROP VIEW IF EXISTS public.v_inventory_consumption_audit CASCADE;
|
|
12938
|
+
|
|
12939
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit
|
|
12940
|
+
WITH (security_invoker = 'true') AS
|
|
12941
|
+
WITH consumed AS (
|
|
12942
|
+
SELECT m.tenant_id,
|
|
12943
|
+
m.unit_id,
|
|
12944
|
+
m.source_item_id AS item_id,
|
|
12945
|
+
m.product_id,
|
|
12946
|
+
-- o raz\xE3o guarda sa\xEDda como negativo; quem confere pensa em positivo
|
|
12947
|
+
sum(- m.quantity) AS posted_quantity,
|
|
12948
|
+
sum(m.total_cost) AS total_cost,
|
|
12949
|
+
min(m.created_at) AS used_at,
|
|
12950
|
+
min(m.movement_date) AS movement_date,
|
|
12951
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
12952
|
+
count(DISTINCT m.source_location_id)::int AS location_count,
|
|
12953
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
12954
|
+
(array_agg(m.metadata ->> 'recipe_id' ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
12955
|
+
FROM public.plg_inventory_stock_movements m
|
|
12956
|
+
WHERE m.source_item_type = 'order_item'
|
|
12957
|
+
AND m.source_item_id IS NOT NULL
|
|
12958
|
+
AND m.kind = 'out'
|
|
12959
|
+
-- estornado n\xE3o \xE9 consumo: devolver ao estoque apaga a linha da fila
|
|
12960
|
+
AND NOT EXISTS (
|
|
12961
|
+
SELECT 1 FROM public.plg_inventory_stock_movements r
|
|
12962
|
+
WHERE r.reverses_movement_id = m.id
|
|
12963
|
+
)
|
|
12964
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
12965
|
+
), corrected AS (
|
|
12966
|
+
SELECT c.tenant_id,
|
|
12967
|
+
c.source_item_id AS item_id,
|
|
12968
|
+
c.product_id,
|
|
12969
|
+
-- devolver ao estoque \xE9 ajuste positivo, e reduz o consumido
|
|
12970
|
+
sum(- c.quantity) AS correction_delta,
|
|
12971
|
+
-- o raz\xE3o guarda custo sempre positivo; o sinal vem da quantidade
|
|
12972
|
+
sum(- sign(c.quantity) * c.total_cost) AS correction_cost,
|
|
12973
|
+
max(c.created_at) AS corrected_at,
|
|
12974
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
12975
|
+
FROM public.plg_inventory_stock_movements c
|
|
12976
|
+
WHERE c.source_item_type = 'usage_audit'
|
|
12977
|
+
AND c.source_item_id IS NOT NULL
|
|
12978
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
12979
|
+
),
|
|
12980
|
+
|
|
12981
|
+
-- \u2500\u2500 origem 1: o consumo que saiu do estoque (a 001, sem uma v\xEDrgula a menos) \u2500
|
|
12982
|
+
ledger AS (
|
|
12983
|
+
SELECT c.tenant_id,
|
|
12984
|
+
c.unit_id,
|
|
12985
|
+
c.item_id,
|
|
12986
|
+
c.product_id,
|
|
12987
|
+
p.name AS product_name,
|
|
12988
|
+
p.sku AS product_sku,
|
|
12989
|
+
c.used_at,
|
|
12990
|
+
c.movement_date,
|
|
12991
|
+
c.posted_quantity,
|
|
12992
|
+
(c.posted_quantity + coalesce(k.correction_delta, 0)) AS net_quantity,
|
|
12993
|
+
std.standard_quantity,
|
|
12994
|
+
CASE WHEN std.standard_quantity IS NULL THEN NULL
|
|
12995
|
+
ELSE ((c.posted_quantity + coalesce(k.correction_delta, 0))
|
|
12996
|
+
- std.standard_quantity)
|
|
12997
|
+
END AS variance,
|
|
12998
|
+
u.abbreviation AS unit_abbreviation,
|
|
12999
|
+
-- o custo acompanha a conclus\xE3o da auditoria, n\xE3o o que foi lan\xE7ado:
|
|
13000
|
+
-- devolver um frasco devolve o custo dele junto
|
|
13001
|
+
(c.total_cost + coalesce(k.correction_cost, 0)) AS total_cost,
|
|
13002
|
+
c.stock_location_id,
|
|
13003
|
+
sl.name AS stock_location_name,
|
|
13004
|
+
-- mais de uma posi\xE7\xE3o quer dizer que o FIFO atravessou lotes
|
|
13005
|
+
c.location_count,
|
|
13006
|
+
r.id AS recipe_id,
|
|
13007
|
+
r.name AS recipe_name,
|
|
13008
|
+
i.order_id,
|
|
13009
|
+
i.name AS item_name,
|
|
13010
|
+
o.reference_number AS order_reference,
|
|
13011
|
+
i.assignee_id AS performed_by_id,
|
|
13012
|
+
perf.name AS performed_by_name,
|
|
13013
|
+
o.party_id AS customer_id,
|
|
13014
|
+
cust.name AS customer_name,
|
|
13015
|
+
k.corrected_at,
|
|
13016
|
+
k.correction_reason,
|
|
13017
|
+
(k.item_id IS NOT NULL) AS is_corrected,
|
|
13018
|
+
'ledger'::text AS source,
|
|
13019
|
+
true AS stock_posted
|
|
13020
|
+
FROM consumed c
|
|
13021
|
+
LEFT JOIN corrected k
|
|
13022
|
+
ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
13023
|
+
LEFT JOIN public.products p ON p.id = c.product_id
|
|
13024
|
+
LEFT JOIN public.plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
13025
|
+
LEFT JOIN public.plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
13026
|
+
-- A linha vendida e seu pedido chegam por LEFT JOIN de prop\xF3sito: quem l\xEA o
|
|
13027
|
+
-- estoque pode n\xE3o ler a venda, e nesse caso a linha aparece sem o nome do
|
|
13028
|
+
-- profissional em vez de desaparecer da confer\xEAncia.
|
|
13029
|
+
LEFT JOIN public.items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
13030
|
+
LEFT JOIN public.orders o ON o.id = i.order_id
|
|
13031
|
+
LEFT JOIN public.people perf ON perf.id = i.assignee_id
|
|
13032
|
+
LEFT JOIN public.people cust ON cust.id = o.party_id
|
|
13033
|
+
LEFT JOIN public.plg_inventory_recipes r
|
|
13034
|
+
ON r.id = nullif(c.recipe_id, '')::uuid AND r.tenant_id = c.tenant_id
|
|
13035
|
+
LEFT JOIN LATERAL (
|
|
13036
|
+
SELECT CASE
|
|
13037
|
+
WHEN r.id IS NOT NULL AND coalesce(r.yield_quantity, 0) > 0 THEN (
|
|
13038
|
+
SELECT round(sum(ri.quantity) * coalesce(i.quantity, 1) / r.yield_quantity, 4)
|
|
13039
|
+
FROM public.plg_inventory_recipe_ingredients ri
|
|
13040
|
+
WHERE ri.tenant_id = c.tenant_id
|
|
13041
|
+
AND ri.recipe_id = r.id
|
|
13042
|
+
AND ri.product_id = c.product_id
|
|
13043
|
+
)
|
|
13044
|
+
-- sem receita o produto vendido \xE9 o pr\xF3prio consumido: o padr\xE3o \xE9
|
|
13045
|
+
-- a quantidade da linha, e divergir a\xED seria erro de sistema
|
|
13046
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
13047
|
+
ELSE NULL
|
|
13048
|
+
END AS standard_quantity
|
|
13049
|
+
) std ON true
|
|
13050
|
+
),
|
|
13051
|
+
|
|
13052
|
+
-- \u2500\u2500 origem 2: a declara\xE7\xE3o do atendimento que nunca virou baixa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13053
|
+
--
|
|
13054
|
+
-- IDENTIDADE. A vista \xE9 chaveada por (item_id, produto), e 116.144 das 116.911
|
|
13055
|
+
-- linhas cl\xEDnicas n\xE3o t\xEAm \`order_item_id\` \u2014 o consumo do V1 pendurava na
|
|
13056
|
+
-- execu\xE7\xE3o, n\xE3o na linha vendida. Ent\xE3o \`item_id\` aqui \xE9 o \`id\` da PR\xD3PRIA
|
|
13057
|
+
-- linha cl\xEDnica. Ela \xE9 \xFAnica, \xE9 est\xE1vel, \xE9 o que a tela usa como chave de linha
|
|
13058
|
+
-- e \xE9 por ela que a corre\xE7\xE3o encontra o registro de volta.
|
|
13059
|
+
--
|
|
13060
|
+
-- O PADR\xC3O VEM DO V1, N\xC3O DA RECEITA. \`default_quantity\` \xE9 literalmente o
|
|
13061
|
+
-- \xABPadr\xE3o: N\xBB que a tela do V1 mostrava \u2014 o n\xFAmero que estava cadastrado no
|
|
13062
|
+
-- servi\xE7o no dia do atendimento. Recalcular pela receita vigente do V2 seria
|
|
13063
|
+
-- comparar o passado com um padr\xE3o que ele n\xE3o teve.
|
|
13064
|
+
--
|
|
13065
|
+
-- \`nullif(default_quantity, 0)\` N\xC3O \xC9 COSM\xC9TICO: 377 linhas vieram com padr\xE3o
|
|
13066
|
+
-- zero, que no V1 quer dizer \xABsem padr\xE3o cadastrado\xBB (a tela testa
|
|
13067
|
+
-- \`default_quantity > 0\` antes de mostrar o selo). Sem o nullif, cada uma delas
|
|
13068
|
+
-- viraria uma diverg\xEAncia do valor inteiro e o filtro \xABs\xF3 divergentes\xBB passaria
|
|
13069
|
+
-- de 172 achados reais para 421 \u2014 afogar o achado \xE9 o mesmo que n\xE3o ter achado.
|
|
13070
|
+
clinical AS (
|
|
13071
|
+
SELECT cs.tenant_id,
|
|
13072
|
+
cs.unit_id,
|
|
13073
|
+
cs.id AS item_id,
|
|
13074
|
+
cs.product_id,
|
|
13075
|
+
p.name AS product_name,
|
|
13076
|
+
p.sku AS product_sku,
|
|
13077
|
+
coalesce(cs.executed_on::timestamptz, cs.created_at) AS used_at,
|
|
13078
|
+
coalesce(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
13079
|
+
-- N\xE3o houve postagem. \xABposted\xBB aqui \xE9 o DECLARADO, e quem diz a verdade
|
|
13080
|
+
-- sobre o raz\xE3o \xE9 \`stock_posted = false\`, logo abaixo. Igual ao V1,
|
|
13081
|
+
-- corrigir reescreve \`used_quantity\` no lugar, ent\xE3o declarado e
|
|
13082
|
+
-- l\xEDquido s\xE3o sempre o mesmo n\xFAmero nesta origem.
|
|
13083
|
+
coalesce(cs.used_quantity, 0) AS posted_quantity,
|
|
13084
|
+
coalesce(cs.used_quantity, 0) AS net_quantity,
|
|
13085
|
+
nullif(cs.default_quantity, 0) AS standard_quantity,
|
|
13086
|
+
CASE WHEN nullif(cs.default_quantity, 0) IS NULL THEN NULL
|
|
13087
|
+
ELSE coalesce(cs.used_quantity, 0) - cs.default_quantity
|
|
13088
|
+
END AS variance,
|
|
13089
|
+
u.abbreviation AS unit_abbreviation,
|
|
13090
|
+
-- custo zero n\xE3o \xE9 \xABn\xE3o sei\xBB: \xE9 \xABnada saiu do estoque, logo nada
|
|
13091
|
+
-- custou ao estoque\xBB. Inventar aqui o pre\xE7o de cat\xE1logo daria \xE0 tela
|
|
13092
|
+
-- um valor que nenhum raz\xE3o jamais escreveu.
|
|
13093
|
+
0::numeric AS total_cost,
|
|
13094
|
+
NULL::uuid AS stock_location_id,
|
|
13095
|
+
NULL::text AS stock_location_name,
|
|
13096
|
+
-- o V1 mostrava \xABSem posi\xE7\xE3o\xBB em 117.024 de 117.024 linhas; a aus\xEAncia
|
|
13097
|
+
-- de local aqui \xE9 o porte fiel daquele selo, n\xE3o uma perda de dado
|
|
13098
|
+
1 AS location_count,
|
|
13099
|
+
NULL::uuid AS recipe_id,
|
|
13100
|
+
NULL::text AS recipe_name,
|
|
13101
|
+
i.order_id,
|
|
13102
|
+
-- o V1 mostrava o SERVI\xC7O na linha; no V2 o servi\xE7o \xE9 um \`products\`
|
|
13103
|
+
sv.name AS item_name,
|
|
13104
|
+
o.reference_number AS order_reference,
|
|
13105
|
+
coalesce(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
13106
|
+
perf.name AS performed_by_name,
|
|
13107
|
+
s.party_id AS customer_id,
|
|
13108
|
+
cust.name AS customer_name,
|
|
13109
|
+
-- \xABCorrigir\xBB e \xABAprovar\xBB eram um ato s\xF3 no V1: auditado \xE9 o conferido
|
|
13110
|
+
cs.audited_at AS corrected_at,
|
|
13111
|
+
cs.audit_notes AS correction_reason,
|
|
13112
|
+
cs.is_audited AS is_corrected,
|
|
13113
|
+
'clinical'::text AS source,
|
|
13114
|
+
false AS stock_posted
|
|
13115
|
+
FROM public.plg_clinical_session_products cs
|
|
13116
|
+
LEFT JOIN public.plg_clinical_sessions s
|
|
13117
|
+
ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id
|
|
13118
|
+
LEFT JOIN public.products p ON p.id = cs.product_id
|
|
13119
|
+
LEFT JOIN public.products sv ON sv.id = s.service_id
|
|
13120
|
+
LEFT JOIN public.plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
13121
|
+
LEFT JOIN public.items i ON i.id = cs.order_item_id AND i.tenant_id = cs.tenant_id
|
|
13122
|
+
LEFT JOIN public.orders o ON o.id = i.order_id
|
|
13123
|
+
LEFT JOIN public.people perf ON perf.id = coalesce(cs.assignee_id, s.performed_by_id, s.assignee_id)
|
|
13124
|
+
LEFT JOIN public.people cust ON cust.id = s.party_id
|
|
13125
|
+
-- A MESMA LINHA N\xC3O CONTA DUAS VEZES. Se a declara\xE7\xE3o cl\xEDnica j\xE1 produziu
|
|
13126
|
+
-- uma sa\xEDda no raz\xE3o, quem manda \xE9 o raz\xE3o: \xE9 ele que tem custo, posi\xE7\xE3o e
|
|
13127
|
+
-- a corre\xE7\xE3o que devolve saldo.
|
|
13128
|
+
--
|
|
13129
|
+
-- O teste de estorno N\xC3O se repete aqui de prop\xF3sito. A 001 decidiu que
|
|
13130
|
+
-- sa\xEDda estornada sai da fila; se o g\xEAmeo cl\xEDnico reaparecesse quando o
|
|
13131
|
+
-- estorno acontece, a mesma linha voltaria \xE0 confer\xEAncia pela porta dos
|
|
13132
|
+
-- fundos e diria o contr\xE1rio do que a 001 disse.
|
|
13133
|
+
WHERE NOT EXISTS (
|
|
13134
|
+
SELECT 1
|
|
13135
|
+
FROM public.plg_inventory_stock_movements m
|
|
13136
|
+
WHERE m.tenant_id = cs.tenant_id
|
|
13137
|
+
AND m.source_item_type = 'order_item'
|
|
13138
|
+
AND m.source_item_id = cs.order_item_id
|
|
13139
|
+
AND m.product_id = cs.product_id
|
|
13140
|
+
AND m.kind = 'out'
|
|
13141
|
+
)
|
|
13142
|
+
)
|
|
13143
|
+
|
|
13144
|
+
-- O casco externo existe pelos TYPMODS. Um UNION apaga \`numeric(14,4)\` e deixa
|
|
13145
|
+
-- \`numeric\` puro, e o \`CREATE OR REPLACE VIEW\` recusa mudan\xE7a de tipo de coluna
|
|
13146
|
+
-- existente \u2014 inclusive typmod. Ent\xE3o cada coluna volta ao tipo que ela j\xE1
|
|
13147
|
+
-- tinha, um a um. \`variance\` fica \`numeric\` SEM typmod porque \xE9 assim que ela
|
|
13148
|
+
-- est\xE1 no cat\xE1logo hoje; apert\xE1-la para \`numeric(14,4)\` seria o mesmo erro pelo
|
|
13149
|
+
-- lado oposto.
|
|
13150
|
+
SELECT a.tenant_id,
|
|
13151
|
+
a.unit_id,
|
|
13152
|
+
a.item_id,
|
|
13153
|
+
a.product_id,
|
|
13154
|
+
a.product_name,
|
|
13155
|
+
a.product_sku,
|
|
13156
|
+
a.used_at,
|
|
13157
|
+
a.movement_date,
|
|
13158
|
+
a.posted_quantity::numeric(14,4) AS posted_quantity,
|
|
13159
|
+
a.net_quantity::numeric(14,4) AS net_quantity,
|
|
13160
|
+
a.standard_quantity::numeric(14,4) AS standard_quantity,
|
|
13161
|
+
a.variance::numeric AS variance,
|
|
13162
|
+
a.unit_abbreviation,
|
|
13163
|
+
a.total_cost::numeric(16,4) AS total_cost,
|
|
13164
|
+
a.stock_location_id,
|
|
13165
|
+
a.stock_location_name,
|
|
13166
|
+
a.location_count,
|
|
13167
|
+
a.recipe_id,
|
|
13168
|
+
a.recipe_name,
|
|
13169
|
+
a.order_id,
|
|
13170
|
+
a.item_name,
|
|
13171
|
+
a.order_reference,
|
|
13172
|
+
a.performed_by_id,
|
|
13173
|
+
a.performed_by_name,
|
|
13174
|
+
a.customer_id,
|
|
13175
|
+
a.customer_name,
|
|
13176
|
+
a.corrected_at,
|
|
13177
|
+
a.correction_reason,
|
|
13178
|
+
a.is_corrected,
|
|
13179
|
+
-- \u2500\u2500 colunas novas, no fim, que \xE9 onde o REPLACE aceita \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13180
|
+
a.source,
|
|
13181
|
+
a.stock_posted
|
|
13182
|
+
FROM (
|
|
13183
|
+
SELECT * FROM ledger
|
|
13184
|
+
UNION ALL
|
|
13185
|
+
SELECT * FROM clinical
|
|
13186
|
+
) a;
|
|
13187
|
+
|
|
13188
|
+
COMMENT ON VIEW public.v_inventory_consumption_audit IS
|
|
13189
|
+
'O consumo declarado em cada atendimento ao lado do padr\xE3o, por linha e produto, nas suas duas origens: o que saiu do raz\xE3o (source=ledger) e o que o V1 declarou e nunca baixou (source=clinical, stock_posted=false). N\xE3o cria fila: os dois lugares j\xE1 guardavam tudo (001, 011).';
|
|
13190
|
+
|
|
13191
|
+
GRANT SELECT ON public.v_inventory_consumption_audit TO authenticated;
|
|
13192
|
+
GRANT SELECT ON public.v_inventory_consumption_audit TO service_role;
|
|
13193
|
+
|
|
13194
|
+
-- \u2500\u2500 \xEDndices: a confer\xEAncia \xE9 sempre (tenant, mais recente primeiro) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13195
|
+
--
|
|
13196
|
+
-- A tela pagina 50 por vez ordenando por \`used_at DESC\` sobre 116.911 linhas.
|
|
13197
|
+
-- Sem isto a vista varre a tabela inteira a cada p\xE1gina.
|
|
13198
|
+
CREATE INDEX IF NOT EXISTS plg_clinical_session_products_audit_idx
|
|
13199
|
+
ON public.plg_clinical_session_products (tenant_id, executed_on DESC, id);
|
|
13200
|
+
|
|
13201
|
+
-- O anti-join da deduplica\xE7\xE3o entra pelo par (linha vendida, produto)
|
|
13202
|
+
CREATE INDEX IF NOT EXISTS plg_clinical_session_products_order_item_idx
|
|
13203
|
+
ON public.plg_clinical_session_products (tenant_id, order_item_id, product_id)
|
|
13204
|
+
WHERE order_item_id IS NOT NULL;
|
|
13205
|
+
|
|
13206
|
+
-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13207
|
+
-- CORRIGIR, NA ORIGEM CL\xCDNICA
|
|
13208
|
+
--
|
|
13209
|
+
-- A 002 fez de \`app.inventory_correct_usage_line\` o miolo \xFAnico: a corre\xE7\xE3o
|
|
13210
|
+
-- avulsa e o lote s\xE3o o mesmo c\xF3digo. Ele procura a baixa no raz\xE3o e, n\xE3o
|
|
13211
|
+
-- achando, levanta \xABno usage of product % on item % to correct\xBB. Com a vista
|
|
13212
|
+
-- servindo 116.890 linhas cl\xEDnicas, esse erro passaria a ser a resposta normal
|
|
13213
|
+
-- da tela \u2014 o bot\xE3o existiria s\xF3 para falhar.
|
|
13214
|
+
--
|
|
13215
|
+
-- O QUE CORRIGIR SIGNIFICA EM CADA ORIGEM:
|
|
13216
|
+
--
|
|
13217
|
+
-- raz\xE3o \u2014 a baixa aconteceu; corrigir 50 para 40 DEVOLVE 10 ao saldo, como
|
|
13218
|
+
-- ajuste, porque o raz\xE3o \xE9 append-only. (inalterado)
|
|
13219
|
+
-- cl\xEDnica \u2014 nada saiu; n\xE3o h\xE1 o que devolver. Corrigir \xE9 o ato do V1:
|
|
13220
|
+
-- conserta a quantidade declarada, exige motivo, e APROVA junto \u2014
|
|
13221
|
+
-- \xABSalvar e Aprovar\xBB era um bot\xE3o s\xF3, nunca dois.
|
|
13222
|
+
--
|
|
13223
|
+
-- Lan\xE7ar um ajuste de estoque para a origem cl\xEDnica seria pior que n\xE3o
|
|
13224
|
+
-- corrigir: criaria movimento para um consumo que nunca teve movimento, e o
|
|
13225
|
+
-- saldo passaria a contar uma devolu\xE7\xE3o de algo que nunca saiu.
|
|
13226
|
+
--
|
|
13227
|
+
-- PERMISS\xC3O, EXPLICITAMENTE. A fun\xE7\xE3o \xE9 SECURITY DEFINER, ent\xE3o o UPDATE n\xE3o
|
|
13228
|
+
-- passa pela RLS de \`plg_clinical_session_products\` \u2014 quem tem de barrar \xE9 este
|
|
13229
|
+
-- c\xF3digo. Exige-se \`inventory.edit\` na unidade do consumo, que \xE9 a mesma regra
|
|
13230
|
+
-- do caminho do raz\xE3o (\`inventory_authorize_location\` cobra \`inventory.edit\` na
|
|
13231
|
+
-- unidade do local). Linha sem unidade (906 delas) cai no teste \xABem qualquer
|
|
13232
|
+
-- unidade\xBB, espelhando o \`unit_id IS NULL OR has_unit(...)\` da pr\xF3pria policy.
|
|
13233
|
+
CREATE OR REPLACE FUNCTION app.inventory_correct_usage_line(
|
|
13234
|
+
p_tenant uuid,
|
|
13235
|
+
p_op uuid,
|
|
13236
|
+
p_key text,
|
|
13237
|
+
p_line_no integer,
|
|
13238
|
+
p_item_id uuid,
|
|
13239
|
+
p_product_id uuid,
|
|
13240
|
+
p_quantity numeric,
|
|
13241
|
+
p_reason text
|
|
13242
|
+
) RETURNS jsonb
|
|
13243
|
+
LANGUAGE plpgsql
|
|
13244
|
+
SECURITY DEFINER
|
|
13245
|
+
SET search_path TO ''
|
|
13246
|
+
AS $$
|
|
13247
|
+
DECLARE
|
|
13248
|
+
v_row record;
|
|
13249
|
+
v_cs record;
|
|
13250
|
+
v_delta numeric;
|
|
13251
|
+
v_unit uuid;
|
|
13252
|
+
v_id uuid;
|
|
13253
|
+
BEGIN
|
|
13254
|
+
IF p_quantity IS NULL OR p_quantity < 0 THEN
|
|
13255
|
+
RAISE EXCEPTION 'inventory: the corrected quantity cannot be negative' USING ERRCODE = '22023';
|
|
13256
|
+
END IF;
|
|
13257
|
+
|
|
13258
|
+
-- o consumido de hoje: a baixa original mais o que auditorias j\xE1 corrigiram
|
|
13259
|
+
SELECT sum(- m.quantity) AS net,
|
|
13260
|
+
(array_agg(m.source_location_id ORDER BY m.created_at DESC, m.id DESC))[1] AS location_id,
|
|
13261
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at DESC, m.id DESC))[1] AS measurement_unit_id,
|
|
13262
|
+
(array_agg(m.batch_number ORDER BY m.created_at DESC, m.id DESC))[1] AS batch_number,
|
|
13263
|
+
(array_agg(m.expiration_date ORDER BY m.created_at DESC, m.id DESC))[1] AS expiration_date,
|
|
13264
|
+
(array_agg(m.unit_cost ORDER BY m.created_at DESC, m.id DESC))[1] AS unit_cost
|
|
13265
|
+
INTO v_row
|
|
13266
|
+
FROM public.plg_inventory_stock_movements m
|
|
13267
|
+
WHERE m.tenant_id = p_tenant
|
|
13268
|
+
AND m.source_item_id = p_item_id
|
|
13269
|
+
AND m.product_id = p_product_id
|
|
13270
|
+
AND ((m.source_item_type = 'order_item' AND m.kind = 'out')
|
|
13271
|
+
OR (m.source_item_type = 'usage_audit' AND m.kind = 'adjust'))
|
|
13272
|
+
AND NOT EXISTS (
|
|
13273
|
+
SELECT 1 FROM public.plg_inventory_stock_movements r
|
|
13274
|
+
WHERE r.reverses_movement_id = m.id
|
|
13275
|
+
);
|
|
13276
|
+
|
|
13277
|
+
-- \u2500\u2500 a origem cl\xEDnica: consumo declarado que nunca virou baixa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13278
|
+
IF v_row.net IS NULL THEN
|
|
13279
|
+
SELECT cs.id, cs.unit_id, cs.used_quantity, cs.is_audited
|
|
13280
|
+
INTO v_cs
|
|
13281
|
+
FROM public.plg_clinical_session_products cs
|
|
13282
|
+
WHERE cs.tenant_id = p_tenant
|
|
13283
|
+
AND cs.id = p_item_id
|
|
13284
|
+
AND cs.product_id = p_product_id;
|
|
13285
|
+
|
|
13286
|
+
IF v_cs.id IS NULL THEN
|
|
13287
|
+
RAISE EXCEPTION 'inventory: no usage of product % on item % to correct', p_product_id, p_item_id
|
|
13288
|
+
USING ERRCODE = '22023';
|
|
13289
|
+
END IF;
|
|
13290
|
+
|
|
13291
|
+
IF v_cs.unit_id IS NOT NULL THEN
|
|
13292
|
+
IF NOT app.has_unit(v_cs.unit_id) THEN
|
|
13293
|
+
RAISE EXCEPTION 'inventory: no access to the unit of usage %', p_item_id USING ERRCODE = '42501';
|
|
13294
|
+
END IF;
|
|
13295
|
+
IF NOT app.has_permission('inventory.edit', v_cs.unit_id) THEN
|
|
13296
|
+
RAISE EXCEPTION 'inventory: inventory.edit required at the unit of usage %', p_item_id
|
|
13297
|
+
USING ERRCODE = '42501';
|
|
13298
|
+
END IF;
|
|
13299
|
+
ELSIF NOT app.has_permission_anywhere('inventory.edit') THEN
|
|
13300
|
+
RAISE EXCEPTION 'inventory: inventory.edit required to correct usage %', p_item_id
|
|
13301
|
+
USING ERRCODE = '42501';
|
|
13302
|
+
END IF;
|
|
13303
|
+
|
|
13304
|
+
-- Conferir e n\xE3o mudar n\xFAmero AINDA \xE9 conferir: a linha pendente vira
|
|
13305
|
+
-- auditada. S\xF3 \xE9 \xABunchanged\xBB o que j\xE1 estava auditado com este n\xFAmero \u2014
|
|
13306
|
+
-- reenviar o mesmo lote n\xE3o reescreve a data da auditoria.
|
|
13307
|
+
IF coalesce(v_cs.used_quantity, 0) = p_quantity AND v_cs.is_audited THEN
|
|
13308
|
+
RETURN jsonb_build_object('status', 'unchanged', 'origin', 'clinical',
|
|
13309
|
+
'item_id', p_item_id, 'product_id', p_product_id,
|
|
13310
|
+
'previous_quantity', coalesce(v_cs.used_quantity, 0),
|
|
13311
|
+
'quantity', p_quantity, 'delta', 0);
|
|
13312
|
+
END IF;
|
|
13313
|
+
|
|
13314
|
+
UPDATE public.plg_clinical_session_products
|
|
13315
|
+
SET used_quantity = p_quantity,
|
|
13316
|
+
is_audited = true,
|
|
13317
|
+
audited_at = now(),
|
|
13318
|
+
audited_by = auth.uid(),
|
|
13319
|
+
audit_notes = p_reason,
|
|
13320
|
+
updated_at = now()
|
|
13321
|
+
WHERE tenant_id = p_tenant AND id = p_item_id AND product_id = p_product_id;
|
|
13322
|
+
|
|
13323
|
+
-- Sem \`movements\`: nada entrou no raz\xE3o, e devolver uma lista vazia \xE9 o que
|
|
13324
|
+
-- diz isso a quem chamou. O lote continua contando a linha como aplicada.
|
|
13325
|
+
RETURN jsonb_build_object('status', 'corrected', 'origin', 'clinical',
|
|
13326
|
+
'item_id', p_item_id, 'product_id', p_product_id,
|
|
13327
|
+
'unit_id', v_cs.unit_id,
|
|
13328
|
+
'previous_quantity', coalesce(v_cs.used_quantity, 0),
|
|
13329
|
+
'quantity', p_quantity,
|
|
13330
|
+
'delta', coalesce(v_cs.used_quantity, 0) - p_quantity,
|
|
13331
|
+
'movements', '[]'::jsonb);
|
|
13332
|
+
END IF;
|
|
13333
|
+
|
|
13334
|
+
-- \u2500\u2500 a origem do raz\xE3o: inalterada desde a 002 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13335
|
+
-- positivo devolve ao estoque, negativo tira mais
|
|
13336
|
+
v_delta := v_row.net - p_quantity;
|
|
13337
|
+
IF v_delta = 0 THEN
|
|
13338
|
+
RETURN jsonb_build_object('status', 'unchanged', 'origin', 'ledger', 'item_id', p_item_id,
|
|
13339
|
+
'product_id', p_product_id, 'previous_quantity', v_row.net,
|
|
13340
|
+
'quantity', p_quantity, 'delta', 0);
|
|
13341
|
+
END IF;
|
|
13342
|
+
|
|
13343
|
+
IF v_delta < 0
|
|
13344
|
+
AND app.inventory_available(p_tenant, p_product_id, v_row.location_id,
|
|
13345
|
+
v_row.batch_number, v_row.expiration_date) < - v_delta THEN
|
|
13346
|
+
RAISE EXCEPTION 'inventory: correcting to % would take product % below its available quantity at location %',
|
|
13347
|
+
p_quantity, p_product_id, v_row.location_id USING ERRCODE = '23514';
|
|
13348
|
+
END IF;
|
|
13349
|
+
|
|
13350
|
+
v_unit := app.inventory_authorize_location(p_tenant, v_row.location_id, 'inventory.edit');
|
|
13351
|
+
|
|
13352
|
+
INSERT INTO public.plg_inventory_stock_movements
|
|
13353
|
+
(tenant_id, product_id, kind, quantity, unit_cost, source_location_id,
|
|
13354
|
+
batch_number, expiration_date, measurement_unit_id, document_type,
|
|
13355
|
+
reason, source_item_type, source_item_id, idempotency_key, line_no,
|
|
13356
|
+
operation_id, metadata)
|
|
13357
|
+
VALUES
|
|
13358
|
+
(p_tenant, p_product_id, 'adjust', v_delta, coalesce(v_row.unit_cost, 0), v_row.location_id,
|
|
13359
|
+
v_row.batch_number, v_row.expiration_date, v_row.measurement_unit_id, 'adjustment',
|
|
13360
|
+
p_reason, 'usage_audit', p_item_id, p_key, p_line_no, p_op,
|
|
13361
|
+
jsonb_build_object('source', 'usage_audit', 'order_item_id', p_item_id,
|
|
13362
|
+
'posted_quantity', v_row.net, 'corrected_quantity', p_quantity))
|
|
13363
|
+
RETURNING id INTO v_id;
|
|
13364
|
+
|
|
13365
|
+
RETURN jsonb_build_object('status', 'corrected', 'origin', 'ledger', 'item_id', p_item_id,
|
|
13366
|
+
'product_id', p_product_id, 'location_id', v_row.location_id,
|
|
13367
|
+
'unit_id', v_unit, 'previous_quantity', v_row.net,
|
|
13368
|
+
'quantity', p_quantity, 'delta', v_delta,
|
|
13369
|
+
'movements', app.inventory_movement_json(v_id));
|
|
13370
|
+
END $$;
|
|
13371
|
+
|
|
13372
|
+
COMMENT ON FUNCTION app.inventory_correct_usage_line(uuid, uuid, text, integer, uuid, uuid, numeric, text) IS
|
|
13373
|
+
'Uma linha de corre\xE7\xE3o de consumo dentro de uma opera\xE7\xE3o j\xE1 aberta. Origem raz\xE3o: lan\xE7a a diferen\xE7a como ajuste. Origem cl\xEDnica (consumo do V1 que nunca baixou): conserta a quantidade declarada e aprova, sem tocar no saldo (002, 011).';
|
|
13374
|
+
|
|
13375
|
+
REVOKE ALL ON FUNCTION app.inventory_correct_usage_line(uuid, uuid, text, integer, uuid, uuid, numeric, text) FROM PUBLIC;
|
|
13376
|
+
`;
|
|
13377
|
+
var MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA = `-- O consumo cl\xEDnico n\xE3o precisa da tabela de itens inteira.
|
|
13378
|
+
--
|
|
13379
|
+
-- A auditoria de uso continuava estourando o \`statement_timeout\` do papel
|
|
13380
|
+
-- \`authenticated\` (8 s) mesmo depois de a clinical/003 baratear a pol\xEDtica. O
|
|
13381
|
+
-- plano, lido com o tempo solto, mostrou onde:
|
|
13382
|
+
--
|
|
13383
|
+
-- Hash (cost=154686.67 rows=23255) (actual rows=455925)
|
|
13384
|
+
--
|
|
13385
|
+
-- O ramo cl\xEDnico da vista faz \`LEFT JOIN items i ON i.id = cs.order_item_id\`, e
|
|
13386
|
+
-- o planejador resolve isso montando um hash de \`public.items\` INTEIRA \u2014
|
|
13387
|
+
-- 455.925 linhas \u2014 para casar com 116.911 consumos. S\xF3 que **767** desses
|
|
13388
|
+
-- consumos t\xEAm \`order_item_id\`: no V1 o produto usado pendura na execu\xE7\xE3o, n\xE3o
|
|
13389
|
+
-- na linha vendida. O hash existe para 0,7% das linhas e \xE9 pago por 100%.
|
|
13390
|
+
--
|
|
13391
|
+
-- O mesmo vale para \`orders\` e para as duas idas a \`people\` (120.808 linhas
|
|
13392
|
+
-- cada uma, uma para quem executou e outra para a cliente).
|
|
13393
|
+
--
|
|
13394
|
+
-- \u2500\u2500 A corre\xE7\xE3o \xE9 a forma do join, n\xE3o o dado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
13395
|
+
--
|
|
13396
|
+
-- Cada um vira LATERAL com a condi\xE7\xE3o de exist\xEAncia dentro. Assim o planejador
|
|
13397
|
+
-- deixa de montar hash e passa a fazer busca por chave prim\xE1ria \u2014 767 buscas em
|
|
13398
|
+
-- vez de um hash de 455 mil linhas \u2014, e as linhas sem \`order_item_id\` n\xE3o
|
|
13399
|
+
-- procuram nada.
|
|
13400
|
+
--
|
|
13401
|
+
-- \`LEFT JOIN LATERAL ... ON true\` preserva a sem\xE2ntica do LEFT JOIN: a linha do
|
|
13402
|
+
-- consumo sobrevive mesmo quando n\xE3o h\xE1 item, que \xE9 o caso de 116.144 delas.
|
|
13403
|
+
--
|
|
13404
|
+
-- Medido no cluster, como o usu\xE1rio que estava com a tela aberta, com ROLLBACK:
|
|
13405
|
+
--
|
|
13406
|
+
-- 50 linhas ordenadas por data 1,94 s (antes: estourava os 8 s)
|
|
13407
|
+
-- contagem total 1,27 s 116.911 linhas \u2014 a MESMA de antes
|
|
13408
|
+
--
|
|
13409
|
+
-- A contagem id\xEAntica \xE9 o teste que importa: uma vista que fica r\xE1pida
|
|
13410
|
+
-- devolvendo menos linha n\xE3o ficou r\xE1pida, ficou errada.
|
|
13411
|
+
--
|
|
13412
|
+
-- Idempotente. \`CREATE OR REPLACE\` mant\xE9m nome, ordem e tipo das colunas \u2014 s\xF3
|
|
13413
|
+
-- os joins mudam \u2014 e mant\xE9m \`security_invoker\`, sem o qual a vista passaria a
|
|
13414
|
+
-- ler com os direitos de quem a criou e furaria a RLS.
|
|
13415
|
+
|
|
13416
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
13417
|
+
WITH consumed AS (
|
|
13418
|
+
SELECT m.tenant_id,
|
|
13419
|
+
m.unit_id,
|
|
13420
|
+
m.source_item_id AS item_id,
|
|
13421
|
+
m.product_id,
|
|
13422
|
+
sum(- m.quantity) AS posted_quantity,
|
|
13423
|
+
sum(m.total_cost) AS total_cost,
|
|
13424
|
+
min(m.created_at) AS used_at,
|
|
13425
|
+
min(m.movement_date) AS movement_date,
|
|
13426
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
13427
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
13428
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
13429
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
13430
|
+
FROM plg_inventory_stock_movements m
|
|
13431
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
13432
|
+
FROM plg_inventory_stock_movements r
|
|
13433
|
+
WHERE r.reverses_movement_id = m.id))
|
|
13434
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
13435
|
+
), corrected AS (
|
|
13436
|
+
SELECT c.tenant_id,
|
|
13437
|
+
c.source_item_id AS item_id,
|
|
13438
|
+
c.product_id,
|
|
13439
|
+
sum(- c.quantity) AS correction_delta,
|
|
13440
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
13441
|
+
max(c.created_at) AS corrected_at,
|
|
13442
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
13443
|
+
FROM plg_inventory_stock_movements c
|
|
13444
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
13445
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
13446
|
+
), ledger AS (
|
|
13447
|
+
SELECT c.tenant_id,
|
|
13448
|
+
c.unit_id,
|
|
13449
|
+
c.item_id,
|
|
13450
|
+
c.product_id,
|
|
13451
|
+
p.name AS product_name,
|
|
13452
|
+
p.sku AS product_sku,
|
|
13453
|
+
c.used_at,
|
|
13454
|
+
c.movement_date,
|
|
13455
|
+
c.posted_quantity,
|
|
13456
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
13457
|
+
std.standard_quantity,
|
|
13458
|
+
CASE
|
|
13459
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
13460
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
13461
|
+
END AS variance,
|
|
13462
|
+
u.abbreviation AS unit_abbreviation,
|
|
13463
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
13464
|
+
c.stock_location_id,
|
|
13465
|
+
sl.name AS stock_location_name,
|
|
13466
|
+
c.location_count,
|
|
13467
|
+
r.id AS recipe_id,
|
|
13468
|
+
r.name AS recipe_name,
|
|
13469
|
+
i.order_id,
|
|
13470
|
+
i.name AS item_name,
|
|
13471
|
+
o.reference_number AS order_reference,
|
|
13472
|
+
i.assignee_id AS performed_by_id,
|
|
13473
|
+
perf.name AS performed_by_name,
|
|
13474
|
+
o.party_id AS customer_id,
|
|
13475
|
+
cust.name AS customer_name,
|
|
13476
|
+
k.corrected_at,
|
|
13477
|
+
k.correction_reason,
|
|
13478
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
13479
|
+
'ledger'::text AS source,
|
|
13480
|
+
true AS stock_posted
|
|
13481
|
+
FROM consumed c
|
|
13482
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
13483
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
13484
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
13485
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
13486
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
13487
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
13488
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
13489
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
13490
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
13491
|
+
LEFT JOIN LATERAL ( SELECT
|
|
13492
|
+
CASE
|
|
13493
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
13494
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
13495
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
13496
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
13497
|
+
ELSE NULL::numeric
|
|
13498
|
+
END AS standard_quantity) std ON true
|
|
13499
|
+
), clinical AS (
|
|
13500
|
+
SELECT cs.tenant_id,
|
|
13501
|
+
cs.unit_id,
|
|
13502
|
+
cs.id AS item_id,
|
|
13503
|
+
cs.product_id,
|
|
13504
|
+
p.name AS product_name,
|
|
13505
|
+
p.sku AS product_sku,
|
|
13506
|
+
COALESCE(cs.executed_on::timestamp with time zone, cs.created_at) AS used_at,
|
|
13507
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
13508
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
13509
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
13510
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
13511
|
+
CASE
|
|
13512
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
13513
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
13514
|
+
END AS variance,
|
|
13515
|
+
u.abbreviation AS unit_abbreviation,
|
|
13516
|
+
0::numeric AS total_cost,
|
|
13517
|
+
NULL::uuid AS stock_location_id,
|
|
13518
|
+
NULL::text AS stock_location_name,
|
|
13519
|
+
1 AS location_count,
|
|
13520
|
+
NULL::uuid AS recipe_id,
|
|
13521
|
+
NULL::text AS recipe_name,
|
|
13522
|
+
i.order_id,
|
|
13523
|
+
sv.name AS item_name,
|
|
13524
|
+
o.reference_number AS order_reference,
|
|
13525
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
13526
|
+
perf.name AS performed_by_name,
|
|
13527
|
+
s.party_id AS customer_id,
|
|
13528
|
+
cust.name AS customer_name,
|
|
13529
|
+
cs.audited_at AS corrected_at,
|
|
13530
|
+
cs.audit_notes AS correction_reason,
|
|
13531
|
+
cs.is_audited AS is_corrected,
|
|
13532
|
+
'clinical'::text AS source,
|
|
13533
|
+
false AS stock_posted
|
|
13534
|
+
FROM plg_clinical_session_products cs
|
|
13535
|
+
LEFT JOIN plg_clinical_sessions s ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id
|
|
13536
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
13537
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
13538
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
13539
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
13540
|
+
FROM items it
|
|
13541
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
13542
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true
|
|
13543
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
13544
|
+
FROM orders ord
|
|
13545
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true
|
|
13546
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
13547
|
+
FROM people pe
|
|
13548
|
+
WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true
|
|
13549
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
13550
|
+
FROM people pe
|
|
13551
|
+
WHERE pe.id = s.party_id) cust ON true
|
|
13552
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
13553
|
+
FROM plg_inventory_stock_movements m
|
|
13554
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
13555
|
+
)
|
|
13556
|
+
SELECT tenant_id,
|
|
13557
|
+
unit_id,
|
|
13558
|
+
item_id,
|
|
13559
|
+
product_id,
|
|
13560
|
+
product_name,
|
|
13561
|
+
product_sku,
|
|
13562
|
+
used_at,
|
|
13563
|
+
movement_date,
|
|
13564
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
13565
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
13566
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
13567
|
+
variance,
|
|
13568
|
+
unit_abbreviation,
|
|
13569
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
13570
|
+
stock_location_id,
|
|
13571
|
+
stock_location_name,
|
|
13572
|
+
location_count,
|
|
13573
|
+
recipe_id,
|
|
13574
|
+
recipe_name,
|
|
13575
|
+
order_id,
|
|
13576
|
+
item_name,
|
|
13577
|
+
order_reference,
|
|
13578
|
+
performed_by_id,
|
|
13579
|
+
performed_by_name,
|
|
13580
|
+
customer_id,
|
|
13581
|
+
customer_name,
|
|
13582
|
+
corrected_at,
|
|
13583
|
+
correction_reason,
|
|
13584
|
+
is_corrected,
|
|
13585
|
+
source,
|
|
13586
|
+
stock_posted
|
|
13587
|
+
FROM ( SELECT ledger.tenant_id,
|
|
13588
|
+
ledger.unit_id,
|
|
13589
|
+
ledger.item_id,
|
|
13590
|
+
ledger.product_id,
|
|
13591
|
+
ledger.product_name,
|
|
13592
|
+
ledger.product_sku,
|
|
13593
|
+
ledger.used_at,
|
|
13594
|
+
ledger.movement_date,
|
|
13595
|
+
ledger.posted_quantity,
|
|
13596
|
+
ledger.net_quantity,
|
|
13597
|
+
ledger.standard_quantity,
|
|
13598
|
+
ledger.variance,
|
|
13599
|
+
ledger.unit_abbreviation,
|
|
13600
|
+
ledger.total_cost,
|
|
13601
|
+
ledger.stock_location_id,
|
|
13602
|
+
ledger.stock_location_name,
|
|
13603
|
+
ledger.location_count,
|
|
13604
|
+
ledger.recipe_id,
|
|
13605
|
+
ledger.recipe_name,
|
|
13606
|
+
ledger.order_id,
|
|
13607
|
+
ledger.item_name,
|
|
13608
|
+
ledger.order_reference,
|
|
13609
|
+
ledger.performed_by_id,
|
|
13610
|
+
ledger.performed_by_name,
|
|
13611
|
+
ledger.customer_id,
|
|
13612
|
+
ledger.customer_name,
|
|
13613
|
+
ledger.corrected_at,
|
|
13614
|
+
ledger.correction_reason,
|
|
13615
|
+
ledger.is_corrected,
|
|
13616
|
+
ledger.source,
|
|
13617
|
+
ledger.stock_posted
|
|
13618
|
+
FROM ledger
|
|
13619
|
+
UNION ALL
|
|
13620
|
+
SELECT clinical.tenant_id,
|
|
13621
|
+
clinical.unit_id,
|
|
13622
|
+
clinical.item_id,
|
|
13623
|
+
clinical.product_id,
|
|
13624
|
+
clinical.product_name,
|
|
13625
|
+
clinical.product_sku,
|
|
13626
|
+
clinical.used_at,
|
|
13627
|
+
clinical.movement_date,
|
|
13628
|
+
clinical.posted_quantity,
|
|
13629
|
+
clinical.net_quantity,
|
|
13630
|
+
clinical.standard_quantity,
|
|
13631
|
+
clinical.variance,
|
|
13632
|
+
clinical.unit_abbreviation,
|
|
13633
|
+
clinical.total_cost,
|
|
13634
|
+
clinical.stock_location_id,
|
|
13635
|
+
clinical.stock_location_name,
|
|
13636
|
+
clinical.location_count,
|
|
13637
|
+
clinical.recipe_id,
|
|
13638
|
+
clinical.recipe_name,
|
|
13639
|
+
clinical.order_id,
|
|
13640
|
+
clinical.item_name,
|
|
13641
|
+
clinical.order_reference,
|
|
13642
|
+
clinical.performed_by_id,
|
|
13643
|
+
clinical.performed_by_name,
|
|
13644
|
+
clinical.customer_id,
|
|
13645
|
+
clinical.customer_name,
|
|
13646
|
+
clinical.corrected_at,
|
|
13647
|
+
clinical.correction_reason,
|
|
13648
|
+
clinical.is_corrected,
|
|
13649
|
+
clinical.source,
|
|
13650
|
+
clinical.stock_posted
|
|
13651
|
+
FROM clinical) a;;
|
|
13652
|
+
`;
|
|
13653
|
+
var MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA = `-- A fila de auditoria tem uma janela, e a janela precisa de \xEDndice.
|
|
13654
|
+
--
|
|
13655
|
+
-- \`v_inventory_consumption_audit\` atravessa a hist\xF3ria inteira do consumo. No Espa\xE7o
|
|
13656
|
+
-- Facial isso \xE9 116.939 linhas, e pedir as 50 mais recentes custava 7 s de
|
|
13657
|
+
-- banco: a tela voltava \`canceling statement due to statement timeout\` e, pior,
|
|
13658
|
+
-- mostrava "Nenhum consumo fora do padr\xE3o" por cima do erro.
|
|
13659
|
+
--
|
|
13660
|
+
-- A tela passou a pedir uma janela (\`used_at >= agora - 30 dias\`), mas o ramo
|
|
13661
|
+
-- cl\xEDnico n\xE3o tinha como responder por ela: \`used_at\` \xE9
|
|
13662
|
+
-- \`coalesce(executed_on::timestamptz, created_at)\` e essa express\xE3o N\xC3O \xE9
|
|
13663
|
+
-- index\xE1vel \u2014 o cast de \`date\` para \`timestamptz\` depende do fuso da sess\xE3o e
|
|
13664
|
+
-- o Postgres o recusa em \xEDndice.
|
|
13665
|
+
--
|
|
13666
|
+
-- \`timezone('UTC', executed_on::timestamp)\` \xE9 o MESMO valor (o cluster roda em
|
|
13667
|
+
-- UTC) e \xE9 imut\xE1vel. Trocada a express\xE3o, o \xEDndice abaixo existe e a janela
|
|
13668
|
+
-- deixa de ser uma varredura.
|
|
13669
|
+
|
|
13670
|
+
CREATE INDEX IF NOT EXISTS plg_clinical_session_products_used_at_idx
|
|
13671
|
+
ON public.plg_clinical_session_products (
|
|
13672
|
+
tenant_id,
|
|
13673
|
+
(COALESCE(timezone('UTC', executed_on::timestamp), created_at)) DESC
|
|
13674
|
+
);
|
|
13675
|
+
|
|
13676
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
13677
|
+
WITH consumed AS (
|
|
13678
|
+
SELECT m.tenant_id,
|
|
13679
|
+
m.unit_id,
|
|
13680
|
+
m.source_item_id AS item_id,
|
|
13681
|
+
m.product_id,
|
|
13682
|
+
sum(- m.quantity) AS posted_quantity,
|
|
13683
|
+
sum(m.total_cost) AS total_cost,
|
|
13684
|
+
min(m.created_at) AS used_at,
|
|
13685
|
+
min(m.movement_date) AS movement_date,
|
|
13686
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
13687
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
13688
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
13689
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
13690
|
+
FROM plg_inventory_stock_movements m
|
|
13691
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
13692
|
+
FROM plg_inventory_stock_movements r
|
|
13693
|
+
WHERE r.reverses_movement_id = m.id))
|
|
13694
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
13695
|
+
), corrected AS (
|
|
13696
|
+
SELECT c.tenant_id,
|
|
13697
|
+
c.source_item_id AS item_id,
|
|
13698
|
+
c.product_id,
|
|
13699
|
+
sum(- c.quantity) AS correction_delta,
|
|
13700
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
13701
|
+
max(c.created_at) AS corrected_at,
|
|
13702
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
13703
|
+
FROM plg_inventory_stock_movements c
|
|
13704
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
13705
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
13706
|
+
), ledger AS (
|
|
13707
|
+
SELECT c.tenant_id,
|
|
13708
|
+
c.unit_id,
|
|
13709
|
+
c.item_id,
|
|
13710
|
+
c.product_id,
|
|
13711
|
+
p.name AS product_name,
|
|
13712
|
+
p.sku AS product_sku,
|
|
13713
|
+
c.used_at,
|
|
13714
|
+
c.movement_date,
|
|
13715
|
+
c.posted_quantity,
|
|
13716
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
13717
|
+
std.standard_quantity,
|
|
13718
|
+
CASE
|
|
13719
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
13720
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
13721
|
+
END AS variance,
|
|
13722
|
+
u.abbreviation AS unit_abbreviation,
|
|
13723
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
13724
|
+
c.stock_location_id,
|
|
13725
|
+
sl.name AS stock_location_name,
|
|
13726
|
+
c.location_count,
|
|
13727
|
+
r.id AS recipe_id,
|
|
13728
|
+
r.name AS recipe_name,
|
|
13729
|
+
i.order_id,
|
|
13730
|
+
i.name AS item_name,
|
|
13731
|
+
o.reference_number AS order_reference,
|
|
13732
|
+
i.assignee_id AS performed_by_id,
|
|
13733
|
+
perf.name AS performed_by_name,
|
|
13734
|
+
o.party_id AS customer_id,
|
|
13735
|
+
cust.name AS customer_name,
|
|
13736
|
+
k.corrected_at,
|
|
13737
|
+
k.correction_reason,
|
|
13738
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
13739
|
+
'ledger'::text AS source,
|
|
13740
|
+
true AS stock_posted
|
|
13741
|
+
FROM consumed c
|
|
13742
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
13743
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
13744
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
13745
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
13746
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
13747
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
13748
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
13749
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
13750
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
13751
|
+
LEFT JOIN LATERAL ( SELECT
|
|
13752
|
+
CASE
|
|
13753
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
13754
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
13755
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
13756
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
13757
|
+
ELSE NULL::numeric
|
|
13758
|
+
END AS standard_quantity) std ON true
|
|
13759
|
+
), clinical AS (
|
|
13760
|
+
SELECT cs.tenant_id,
|
|
13761
|
+
cs.unit_id,
|
|
13762
|
+
cs.id AS item_id,
|
|
13763
|
+
cs.product_id,
|
|
13764
|
+
p.name AS product_name,
|
|
13765
|
+
p.sku AS product_sku,
|
|
13766
|
+
COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,
|
|
13767
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
13768
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
13769
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
13770
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
13771
|
+
CASE
|
|
13772
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
13773
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
13774
|
+
END AS variance,
|
|
13775
|
+
u.abbreviation AS unit_abbreviation,
|
|
13776
|
+
0::numeric AS total_cost,
|
|
13777
|
+
NULL::uuid AS stock_location_id,
|
|
13778
|
+
NULL::text AS stock_location_name,
|
|
13779
|
+
1 AS location_count,
|
|
13780
|
+
NULL::uuid AS recipe_id,
|
|
13781
|
+
NULL::text AS recipe_name,
|
|
13782
|
+
i.order_id,
|
|
13783
|
+
sv.name AS item_name,
|
|
13784
|
+
o.reference_number AS order_reference,
|
|
13785
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
13786
|
+
perf.name AS performed_by_name,
|
|
13787
|
+
s.party_id AS customer_id,
|
|
13788
|
+
cust.name AS customer_name,
|
|
13789
|
+
cs.audited_at AS corrected_at,
|
|
13790
|
+
cs.audit_notes AS correction_reason,
|
|
13791
|
+
cs.is_audited AS is_corrected,
|
|
13792
|
+
'clinical'::text AS source,
|
|
13793
|
+
false AS stock_posted
|
|
13794
|
+
FROM plg_clinical_session_products cs
|
|
13795
|
+
LEFT JOIN plg_clinical_sessions s ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id
|
|
13796
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
13797
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
13798
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
13799
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
13800
|
+
FROM items it
|
|
13801
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
13802
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true
|
|
13803
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
13804
|
+
FROM orders ord
|
|
13805
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true
|
|
13806
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
13807
|
+
FROM people pe
|
|
13808
|
+
WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true
|
|
13809
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
13810
|
+
FROM people pe
|
|
13811
|
+
WHERE pe.id = s.party_id) cust ON true
|
|
13812
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
13813
|
+
FROM plg_inventory_stock_movements m
|
|
13814
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
13815
|
+
)
|
|
13816
|
+
SELECT tenant_id,
|
|
13817
|
+
unit_id,
|
|
13818
|
+
item_id,
|
|
13819
|
+
product_id,
|
|
13820
|
+
product_name,
|
|
13821
|
+
product_sku,
|
|
13822
|
+
used_at,
|
|
13823
|
+
movement_date,
|
|
13824
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
13825
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
13826
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
13827
|
+
variance,
|
|
13828
|
+
unit_abbreviation,
|
|
13829
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
13830
|
+
stock_location_id,
|
|
13831
|
+
stock_location_name,
|
|
13832
|
+
location_count,
|
|
13833
|
+
recipe_id,
|
|
13834
|
+
recipe_name,
|
|
13835
|
+
order_id,
|
|
13836
|
+
item_name,
|
|
13837
|
+
order_reference,
|
|
13838
|
+
performed_by_id,
|
|
13839
|
+
performed_by_name,
|
|
13840
|
+
customer_id,
|
|
13841
|
+
customer_name,
|
|
13842
|
+
corrected_at,
|
|
13843
|
+
correction_reason,
|
|
13844
|
+
is_corrected,
|
|
13845
|
+
source,
|
|
13846
|
+
stock_posted
|
|
13847
|
+
FROM ( SELECT ledger.tenant_id,
|
|
13848
|
+
ledger.unit_id,
|
|
13849
|
+
ledger.item_id,
|
|
13850
|
+
ledger.product_id,
|
|
13851
|
+
ledger.product_name,
|
|
13852
|
+
ledger.product_sku,
|
|
13853
|
+
ledger.used_at,
|
|
13854
|
+
ledger.movement_date,
|
|
13855
|
+
ledger.posted_quantity,
|
|
13856
|
+
ledger.net_quantity,
|
|
13857
|
+
ledger.standard_quantity,
|
|
13858
|
+
ledger.variance,
|
|
13859
|
+
ledger.unit_abbreviation,
|
|
13860
|
+
ledger.total_cost,
|
|
13861
|
+
ledger.stock_location_id,
|
|
13862
|
+
ledger.stock_location_name,
|
|
13863
|
+
ledger.location_count,
|
|
13864
|
+
ledger.recipe_id,
|
|
13865
|
+
ledger.recipe_name,
|
|
13866
|
+
ledger.order_id,
|
|
13867
|
+
ledger.item_name,
|
|
13868
|
+
ledger.order_reference,
|
|
13869
|
+
ledger.performed_by_id,
|
|
13870
|
+
ledger.performed_by_name,
|
|
13871
|
+
ledger.customer_id,
|
|
13872
|
+
ledger.customer_name,
|
|
13873
|
+
ledger.corrected_at,
|
|
13874
|
+
ledger.correction_reason,
|
|
13875
|
+
ledger.is_corrected,
|
|
13876
|
+
ledger.source,
|
|
13877
|
+
ledger.stock_posted
|
|
13878
|
+
FROM ledger
|
|
13879
|
+
UNION ALL
|
|
13880
|
+
SELECT clinical.tenant_id,
|
|
13881
|
+
clinical.unit_id,
|
|
13882
|
+
clinical.item_id,
|
|
13883
|
+
clinical.product_id,
|
|
13884
|
+
clinical.product_name,
|
|
13885
|
+
clinical.product_sku,
|
|
13886
|
+
clinical.used_at,
|
|
13887
|
+
clinical.movement_date,
|
|
13888
|
+
clinical.posted_quantity,
|
|
13889
|
+
clinical.net_quantity,
|
|
13890
|
+
clinical.standard_quantity,
|
|
13891
|
+
clinical.variance,
|
|
13892
|
+
clinical.unit_abbreviation,
|
|
13893
|
+
clinical.total_cost,
|
|
13894
|
+
clinical.stock_location_id,
|
|
13895
|
+
clinical.stock_location_name,
|
|
13896
|
+
clinical.location_count,
|
|
13897
|
+
clinical.recipe_id,
|
|
13898
|
+
clinical.recipe_name,
|
|
13899
|
+
clinical.order_id,
|
|
13900
|
+
clinical.item_name,
|
|
13901
|
+
clinical.order_reference,
|
|
13902
|
+
clinical.performed_by_id,
|
|
13903
|
+
clinical.performed_by_name,
|
|
13904
|
+
clinical.customer_id,
|
|
13905
|
+
clinical.customer_name,
|
|
13906
|
+
clinical.corrected_at,
|
|
13907
|
+
clinical.correction_reason,
|
|
13908
|
+
clinical.is_corrected,
|
|
13909
|
+
clinical.source,
|
|
13910
|
+
clinical.stock_posted
|
|
13911
|
+
FROM clinical) a;;
|
|
13912
|
+
`;
|
|
13913
|
+
var MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA = `-- O atendimento entra na fila UM A UM, n\xE3o a tabela inteira.
|
|
13914
|
+
--
|
|
13915
|
+
-- A vista buscava a sess\xE3o de cada consumo com um LEFT JOIN comum. Sob RLS
|
|
13916
|
+
-- isso \xE9 caro de um jeito que n\xE3o aparece no EXPLAIN de quem tem chave de
|
|
13917
|
+
-- servi\xE7o: a pol\xEDtica de \`plg_clinical_sessions\` chama \`app.has_unit\` e
|
|
13918
|
+
-- \`app.has_permission\` POR LINHA, e o planejador varre a tabela inteira antes
|
|
13919
|
+
-- de casar com os poucos consumos da janela. Medido aqui: 232.264 chamadas,
|
|
13920
|
+
-- 6,4 s \u2014 e a tela voltava \`canceling statement due to statement timeout\` com
|
|
13921
|
+
-- a janela de 30 dias j\xE1 aplicada.
|
|
13922
|
+
--
|
|
13923
|
+
-- LATERAL amarra a busca \xE0 linha de fora: a sess\xE3o \xE9 procurada pela chave
|
|
13924
|
+
-- (tenant, id) uma vez por consumo, e a RLS roda dezenas de vezes em vez de
|
|
13925
|
+
-- centenas de milhares. \xC9 a mesma corre\xE7\xE3o que a 012 j\xE1 tinha feito para
|
|
13926
|
+
-- \`items\`, \`orders\` e \`people\` \u2014 a sess\xE3o tinha ficado de fora.
|
|
13927
|
+
|
|
13928
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
13929
|
+
WITH consumed AS (
|
|
13930
|
+
SELECT m.tenant_id,
|
|
13931
|
+
m.unit_id,
|
|
13932
|
+
m.source_item_id AS item_id,
|
|
13933
|
+
m.product_id,
|
|
13934
|
+
sum(- m.quantity) AS posted_quantity,
|
|
13935
|
+
sum(m.total_cost) AS total_cost,
|
|
13936
|
+
min(m.created_at) AS used_at,
|
|
13937
|
+
min(m.movement_date) AS movement_date,
|
|
13938
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
13939
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
13940
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
13941
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
13942
|
+
FROM plg_inventory_stock_movements m
|
|
13943
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
13944
|
+
FROM plg_inventory_stock_movements r
|
|
13945
|
+
WHERE r.reverses_movement_id = m.id))
|
|
13946
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
13947
|
+
), corrected AS (
|
|
13948
|
+
SELECT c.tenant_id,
|
|
13949
|
+
c.source_item_id AS item_id,
|
|
13950
|
+
c.product_id,
|
|
13951
|
+
sum(- c.quantity) AS correction_delta,
|
|
13952
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
13953
|
+
max(c.created_at) AS corrected_at,
|
|
13954
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
13955
|
+
FROM plg_inventory_stock_movements c
|
|
13956
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
13957
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
13958
|
+
), ledger AS (
|
|
13959
|
+
SELECT c.tenant_id,
|
|
13960
|
+
c.unit_id,
|
|
13961
|
+
c.item_id,
|
|
13962
|
+
c.product_id,
|
|
13963
|
+
p.name AS product_name,
|
|
13964
|
+
p.sku AS product_sku,
|
|
13965
|
+
c.used_at,
|
|
13966
|
+
c.movement_date,
|
|
13967
|
+
c.posted_quantity,
|
|
13968
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
13969
|
+
std.standard_quantity,
|
|
13970
|
+
CASE
|
|
13971
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
13972
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
13973
|
+
END AS variance,
|
|
13974
|
+
u.abbreviation AS unit_abbreviation,
|
|
13975
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
13976
|
+
c.stock_location_id,
|
|
13977
|
+
sl.name AS stock_location_name,
|
|
13978
|
+
c.location_count,
|
|
13979
|
+
r.id AS recipe_id,
|
|
13980
|
+
r.name AS recipe_name,
|
|
13981
|
+
i.order_id,
|
|
13982
|
+
i.name AS item_name,
|
|
13983
|
+
o.reference_number AS order_reference,
|
|
13984
|
+
i.assignee_id AS performed_by_id,
|
|
13985
|
+
perf.name AS performed_by_name,
|
|
13986
|
+
o.party_id AS customer_id,
|
|
13987
|
+
cust.name AS customer_name,
|
|
13988
|
+
k.corrected_at,
|
|
13989
|
+
k.correction_reason,
|
|
13990
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
13991
|
+
'ledger'::text AS source,
|
|
13992
|
+
true AS stock_posted
|
|
13993
|
+
FROM consumed c
|
|
13994
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
13995
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
13996
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
13997
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
13998
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
13999
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
14000
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
14001
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
14002
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
14003
|
+
LEFT JOIN LATERAL ( SELECT
|
|
14004
|
+
CASE
|
|
14005
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
14006
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
14007
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
14008
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
14009
|
+
ELSE NULL::numeric
|
|
14010
|
+
END AS standard_quantity) std ON true
|
|
14011
|
+
), clinical AS (
|
|
14012
|
+
SELECT cs.tenant_id,
|
|
14013
|
+
cs.unit_id,
|
|
14014
|
+
cs.id AS item_id,
|
|
14015
|
+
cs.product_id,
|
|
14016
|
+
p.name AS product_name,
|
|
14017
|
+
p.sku AS product_sku,
|
|
14018
|
+
COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,
|
|
14019
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
14020
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
14021
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
14022
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
14023
|
+
CASE
|
|
14024
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
14025
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
14026
|
+
END AS variance,
|
|
14027
|
+
u.abbreviation AS unit_abbreviation,
|
|
14028
|
+
0::numeric AS total_cost,
|
|
14029
|
+
NULL::uuid AS stock_location_id,
|
|
14030
|
+
NULL::text AS stock_location_name,
|
|
14031
|
+
1 AS location_count,
|
|
14032
|
+
NULL::uuid AS recipe_id,
|
|
14033
|
+
NULL::text AS recipe_name,
|
|
14034
|
+
i.order_id,
|
|
14035
|
+
sv.name AS item_name,
|
|
14036
|
+
o.reference_number AS order_reference,
|
|
14037
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
14038
|
+
perf.name AS performed_by_name,
|
|
14039
|
+
s.party_id AS customer_id,
|
|
14040
|
+
cust.name AS customer_name,
|
|
14041
|
+
cs.audited_at AS corrected_at,
|
|
14042
|
+
cs.audit_notes AS correction_reason,
|
|
14043
|
+
cs.is_audited AS is_corrected,
|
|
14044
|
+
'clinical'::text AS source,
|
|
14045
|
+
false AS stock_posted
|
|
14046
|
+
FROM plg_clinical_session_products cs
|
|
14047
|
+
LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id
|
|
14048
|
+
FROM plg_clinical_sessions ss
|
|
14049
|
+
WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id) s ON true
|
|
14050
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
14051
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
14052
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
14053
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
14054
|
+
FROM items it
|
|
14055
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
14056
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true
|
|
14057
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
14058
|
+
FROM orders ord
|
|
14059
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true
|
|
14060
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14061
|
+
FROM people pe
|
|
14062
|
+
WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true
|
|
14063
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14064
|
+
FROM people pe
|
|
14065
|
+
WHERE pe.id = s.party_id) cust ON true
|
|
14066
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
14067
|
+
FROM plg_inventory_stock_movements m
|
|
14068
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
14069
|
+
)
|
|
14070
|
+
SELECT tenant_id,
|
|
14071
|
+
unit_id,
|
|
14072
|
+
item_id,
|
|
14073
|
+
product_id,
|
|
14074
|
+
product_name,
|
|
14075
|
+
product_sku,
|
|
14076
|
+
used_at,
|
|
14077
|
+
movement_date,
|
|
14078
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
14079
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
14080
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
14081
|
+
variance,
|
|
14082
|
+
unit_abbreviation,
|
|
14083
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
14084
|
+
stock_location_id,
|
|
14085
|
+
stock_location_name,
|
|
14086
|
+
location_count,
|
|
14087
|
+
recipe_id,
|
|
14088
|
+
recipe_name,
|
|
14089
|
+
order_id,
|
|
14090
|
+
item_name,
|
|
14091
|
+
order_reference,
|
|
14092
|
+
performed_by_id,
|
|
14093
|
+
performed_by_name,
|
|
14094
|
+
customer_id,
|
|
14095
|
+
customer_name,
|
|
14096
|
+
corrected_at,
|
|
14097
|
+
correction_reason,
|
|
14098
|
+
is_corrected,
|
|
14099
|
+
source,
|
|
14100
|
+
stock_posted
|
|
14101
|
+
FROM ( SELECT ledger.tenant_id,
|
|
14102
|
+
ledger.unit_id,
|
|
14103
|
+
ledger.item_id,
|
|
14104
|
+
ledger.product_id,
|
|
14105
|
+
ledger.product_name,
|
|
14106
|
+
ledger.product_sku,
|
|
14107
|
+
ledger.used_at,
|
|
14108
|
+
ledger.movement_date,
|
|
14109
|
+
ledger.posted_quantity,
|
|
14110
|
+
ledger.net_quantity,
|
|
14111
|
+
ledger.standard_quantity,
|
|
14112
|
+
ledger.variance,
|
|
14113
|
+
ledger.unit_abbreviation,
|
|
14114
|
+
ledger.total_cost,
|
|
14115
|
+
ledger.stock_location_id,
|
|
14116
|
+
ledger.stock_location_name,
|
|
14117
|
+
ledger.location_count,
|
|
14118
|
+
ledger.recipe_id,
|
|
14119
|
+
ledger.recipe_name,
|
|
14120
|
+
ledger.order_id,
|
|
14121
|
+
ledger.item_name,
|
|
14122
|
+
ledger.order_reference,
|
|
14123
|
+
ledger.performed_by_id,
|
|
14124
|
+
ledger.performed_by_name,
|
|
14125
|
+
ledger.customer_id,
|
|
14126
|
+
ledger.customer_name,
|
|
14127
|
+
ledger.corrected_at,
|
|
14128
|
+
ledger.correction_reason,
|
|
14129
|
+
ledger.is_corrected,
|
|
14130
|
+
ledger.source,
|
|
14131
|
+
ledger.stock_posted
|
|
14132
|
+
FROM ledger
|
|
14133
|
+
UNION ALL
|
|
14134
|
+
SELECT clinical.tenant_id,
|
|
14135
|
+
clinical.unit_id,
|
|
14136
|
+
clinical.item_id,
|
|
14137
|
+
clinical.product_id,
|
|
14138
|
+
clinical.product_name,
|
|
14139
|
+
clinical.product_sku,
|
|
14140
|
+
clinical.used_at,
|
|
14141
|
+
clinical.movement_date,
|
|
14142
|
+
clinical.posted_quantity,
|
|
14143
|
+
clinical.net_quantity,
|
|
14144
|
+
clinical.standard_quantity,
|
|
14145
|
+
clinical.variance,
|
|
14146
|
+
clinical.unit_abbreviation,
|
|
14147
|
+
clinical.total_cost,
|
|
14148
|
+
clinical.stock_location_id,
|
|
14149
|
+
clinical.stock_location_name,
|
|
14150
|
+
clinical.location_count,
|
|
14151
|
+
clinical.recipe_id,
|
|
14152
|
+
clinical.recipe_name,
|
|
14153
|
+
clinical.order_id,
|
|
14154
|
+
clinical.item_name,
|
|
14155
|
+
clinical.order_reference,
|
|
14156
|
+
clinical.performed_by_id,
|
|
14157
|
+
clinical.performed_by_name,
|
|
14158
|
+
clinical.customer_id,
|
|
14159
|
+
clinical.customer_name,
|
|
14160
|
+
clinical.corrected_at,
|
|
14161
|
+
clinical.correction_reason,
|
|
14162
|
+
clinical.is_corrected,
|
|
14163
|
+
clinical.source,
|
|
14164
|
+
clinical.stock_posted
|
|
14165
|
+
FROM clinical) a;;
|
|
14166
|
+
`;
|
|
14167
|
+
var MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL = `-- LATERAL sem cerca n\xE3o \xE9 LATERAL.
|
|
14168
|
+
--
|
|
14169
|
+
-- A 012 e a 014 escreveram as buscas de \`items\`, \`orders\`, \`people\` e da
|
|
14170
|
+
-- sess\xE3o como LATERAL para que cada uma rodasse por linha. O planejador
|
|
14171
|
+
-- desfez o gesto: subconsulta simples \xE9 ACHATADA de volta a jun\xE7\xE3o comum, e o
|
|
14172
|
+
-- plano voltava a varrer \`people\` inteira (61.854 linhas) e a sess\xE3o inteira.
|
|
14173
|
+
-- Sob RLS cada uma dessas linhas paga \`app.has_unit\` + \`app.has_permission\`,
|
|
14174
|
+
-- e a tela morria nos 8 s do PostgREST.
|
|
14175
|
+
--
|
|
14176
|
+
-- \`LIMIT 1\` \xE9 a cerca: o Postgres n\xE3o achata subconsulta com limite. A chave
|
|
14177
|
+
-- \xE9 \xFAnica nos quatro casos, ent\xE3o o limite n\xE3o muda resultado nenhum \u2014 s\xF3
|
|
14178
|
+
-- impede que a busca de UMA linha vire uma varredura de todas.
|
|
14179
|
+
|
|
14180
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
14181
|
+
WITH consumed AS (
|
|
14182
|
+
SELECT m.tenant_id,
|
|
14183
|
+
m.unit_id,
|
|
14184
|
+
m.source_item_id AS item_id,
|
|
14185
|
+
m.product_id,
|
|
14186
|
+
sum(- m.quantity) AS posted_quantity,
|
|
14187
|
+
sum(m.total_cost) AS total_cost,
|
|
14188
|
+
min(m.created_at) AS used_at,
|
|
14189
|
+
min(m.movement_date) AS movement_date,
|
|
14190
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
14191
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
14192
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
14193
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
14194
|
+
FROM plg_inventory_stock_movements m
|
|
14195
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
14196
|
+
FROM plg_inventory_stock_movements r
|
|
14197
|
+
WHERE r.reverses_movement_id = m.id))
|
|
14198
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
14199
|
+
), corrected AS (
|
|
14200
|
+
SELECT c.tenant_id,
|
|
14201
|
+
c.source_item_id AS item_id,
|
|
14202
|
+
c.product_id,
|
|
14203
|
+
sum(- c.quantity) AS correction_delta,
|
|
14204
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
14205
|
+
max(c.created_at) AS corrected_at,
|
|
14206
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
14207
|
+
FROM plg_inventory_stock_movements c
|
|
14208
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
14209
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
14210
|
+
), ledger AS (
|
|
14211
|
+
SELECT c.tenant_id,
|
|
14212
|
+
c.unit_id,
|
|
14213
|
+
c.item_id,
|
|
14214
|
+
c.product_id,
|
|
14215
|
+
p.name AS product_name,
|
|
14216
|
+
p.sku AS product_sku,
|
|
14217
|
+
c.used_at,
|
|
14218
|
+
c.movement_date,
|
|
14219
|
+
c.posted_quantity,
|
|
14220
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
14221
|
+
std.standard_quantity,
|
|
14222
|
+
CASE
|
|
14223
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
14224
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
14225
|
+
END AS variance,
|
|
14226
|
+
u.abbreviation AS unit_abbreviation,
|
|
14227
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
14228
|
+
c.stock_location_id,
|
|
14229
|
+
sl.name AS stock_location_name,
|
|
14230
|
+
c.location_count,
|
|
14231
|
+
r.id AS recipe_id,
|
|
14232
|
+
r.name AS recipe_name,
|
|
14233
|
+
i.order_id,
|
|
14234
|
+
i.name AS item_name,
|
|
14235
|
+
o.reference_number AS order_reference,
|
|
14236
|
+
i.assignee_id AS performed_by_id,
|
|
14237
|
+
perf.name AS performed_by_name,
|
|
14238
|
+
o.party_id AS customer_id,
|
|
14239
|
+
cust.name AS customer_name,
|
|
14240
|
+
k.corrected_at,
|
|
14241
|
+
k.correction_reason,
|
|
14242
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
14243
|
+
'ledger'::text AS source,
|
|
14244
|
+
true AS stock_posted
|
|
14245
|
+
FROM consumed c
|
|
14246
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
14247
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
14248
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
14249
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
14250
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
14251
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
14252
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
14253
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
14254
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
14255
|
+
LEFT JOIN LATERAL ( SELECT
|
|
14256
|
+
CASE
|
|
14257
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
14258
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
14259
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
14260
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
14261
|
+
ELSE NULL::numeric
|
|
14262
|
+
END AS standard_quantity) std ON true
|
|
14263
|
+
), clinical AS (
|
|
14264
|
+
SELECT cs.tenant_id,
|
|
14265
|
+
cs.unit_id,
|
|
14266
|
+
cs.id AS item_id,
|
|
14267
|
+
cs.product_id,
|
|
14268
|
+
p.name AS product_name,
|
|
14269
|
+
p.sku AS product_sku,
|
|
14270
|
+
COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,
|
|
14271
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
14272
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
14273
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
14274
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
14275
|
+
CASE
|
|
14276
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
14277
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
14278
|
+
END AS variance,
|
|
14279
|
+
u.abbreviation AS unit_abbreviation,
|
|
14280
|
+
0::numeric AS total_cost,
|
|
14281
|
+
NULL::uuid AS stock_location_id,
|
|
14282
|
+
NULL::text AS stock_location_name,
|
|
14283
|
+
1 AS location_count,
|
|
14284
|
+
NULL::uuid AS recipe_id,
|
|
14285
|
+
NULL::text AS recipe_name,
|
|
14286
|
+
i.order_id,
|
|
14287
|
+
sv.name AS item_name,
|
|
14288
|
+
o.reference_number AS order_reference,
|
|
14289
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
14290
|
+
perf.name AS performed_by_name,
|
|
14291
|
+
s.party_id AS customer_id,
|
|
14292
|
+
cust.name AS customer_name,
|
|
14293
|
+
cs.audited_at AS corrected_at,
|
|
14294
|
+
cs.audit_notes AS correction_reason,
|
|
14295
|
+
cs.is_audited AS is_corrected,
|
|
14296
|
+
'clinical'::text AS source,
|
|
14297
|
+
false AS stock_posted
|
|
14298
|
+
FROM plg_clinical_session_products cs
|
|
14299
|
+
LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id
|
|
14300
|
+
FROM plg_clinical_sessions ss
|
|
14301
|
+
WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true
|
|
14302
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
14303
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
14304
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
14305
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
14306
|
+
FROM items it
|
|
14307
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
14308
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true
|
|
14309
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
14310
|
+
FROM orders ord
|
|
14311
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true
|
|
14312
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14313
|
+
FROM people pe
|
|
14314
|
+
WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) LIMIT 1) perf ON true
|
|
14315
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14316
|
+
FROM people pe
|
|
14317
|
+
WHERE pe.id = s.party_id LIMIT 1) cust ON true
|
|
14318
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
14319
|
+
FROM plg_inventory_stock_movements m
|
|
14320
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
14321
|
+
)
|
|
14322
|
+
SELECT tenant_id,
|
|
14323
|
+
unit_id,
|
|
14324
|
+
item_id,
|
|
14325
|
+
product_id,
|
|
14326
|
+
product_name,
|
|
14327
|
+
product_sku,
|
|
14328
|
+
used_at,
|
|
14329
|
+
movement_date,
|
|
14330
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
14331
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
14332
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
14333
|
+
variance,
|
|
14334
|
+
unit_abbreviation,
|
|
14335
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
14336
|
+
stock_location_id,
|
|
14337
|
+
stock_location_name,
|
|
14338
|
+
location_count,
|
|
14339
|
+
recipe_id,
|
|
14340
|
+
recipe_name,
|
|
14341
|
+
order_id,
|
|
14342
|
+
item_name,
|
|
14343
|
+
order_reference,
|
|
14344
|
+
performed_by_id,
|
|
14345
|
+
performed_by_name,
|
|
14346
|
+
customer_id,
|
|
14347
|
+
customer_name,
|
|
14348
|
+
corrected_at,
|
|
14349
|
+
correction_reason,
|
|
14350
|
+
is_corrected,
|
|
14351
|
+
source,
|
|
14352
|
+
stock_posted
|
|
14353
|
+
FROM ( SELECT ledger.tenant_id,
|
|
14354
|
+
ledger.unit_id,
|
|
14355
|
+
ledger.item_id,
|
|
14356
|
+
ledger.product_id,
|
|
14357
|
+
ledger.product_name,
|
|
14358
|
+
ledger.product_sku,
|
|
14359
|
+
ledger.used_at,
|
|
14360
|
+
ledger.movement_date,
|
|
14361
|
+
ledger.posted_quantity,
|
|
14362
|
+
ledger.net_quantity,
|
|
14363
|
+
ledger.standard_quantity,
|
|
14364
|
+
ledger.variance,
|
|
14365
|
+
ledger.unit_abbreviation,
|
|
14366
|
+
ledger.total_cost,
|
|
14367
|
+
ledger.stock_location_id,
|
|
14368
|
+
ledger.stock_location_name,
|
|
14369
|
+
ledger.location_count,
|
|
14370
|
+
ledger.recipe_id,
|
|
14371
|
+
ledger.recipe_name,
|
|
14372
|
+
ledger.order_id,
|
|
14373
|
+
ledger.item_name,
|
|
14374
|
+
ledger.order_reference,
|
|
14375
|
+
ledger.performed_by_id,
|
|
14376
|
+
ledger.performed_by_name,
|
|
14377
|
+
ledger.customer_id,
|
|
14378
|
+
ledger.customer_name,
|
|
14379
|
+
ledger.corrected_at,
|
|
14380
|
+
ledger.correction_reason,
|
|
14381
|
+
ledger.is_corrected,
|
|
14382
|
+
ledger.source,
|
|
14383
|
+
ledger.stock_posted
|
|
14384
|
+
FROM ledger
|
|
14385
|
+
UNION ALL
|
|
14386
|
+
SELECT clinical.tenant_id,
|
|
14387
|
+
clinical.unit_id,
|
|
14388
|
+
clinical.item_id,
|
|
14389
|
+
clinical.product_id,
|
|
14390
|
+
clinical.product_name,
|
|
14391
|
+
clinical.product_sku,
|
|
14392
|
+
clinical.used_at,
|
|
14393
|
+
clinical.movement_date,
|
|
14394
|
+
clinical.posted_quantity,
|
|
14395
|
+
clinical.net_quantity,
|
|
14396
|
+
clinical.standard_quantity,
|
|
14397
|
+
clinical.variance,
|
|
14398
|
+
clinical.unit_abbreviation,
|
|
14399
|
+
clinical.total_cost,
|
|
14400
|
+
clinical.stock_location_id,
|
|
14401
|
+
clinical.stock_location_name,
|
|
14402
|
+
clinical.location_count,
|
|
14403
|
+
clinical.recipe_id,
|
|
14404
|
+
clinical.recipe_name,
|
|
14405
|
+
clinical.order_id,
|
|
14406
|
+
clinical.item_name,
|
|
14407
|
+
clinical.order_reference,
|
|
14408
|
+
clinical.performed_by_id,
|
|
14409
|
+
clinical.performed_by_name,
|
|
14410
|
+
clinical.customer_id,
|
|
14411
|
+
clinical.customer_name,
|
|
14412
|
+
clinical.corrected_at,
|
|
14413
|
+
clinical.correction_reason,
|
|
14414
|
+
clinical.is_corrected,
|
|
14415
|
+
clinical.source,
|
|
14416
|
+
clinical.stock_posted
|
|
14417
|
+
FROM clinical) a;;
|
|
14418
|
+
`;
|
|
14419
|
+
var MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA = `-- A data do consumo passa a ser uma COLUNA, e por isso a janela usa \xEDndice.
|
|
14420
|
+
--
|
|
14421
|
+
-- Sob RLS o Postgres roda as regras da pol\xEDtica ANTES das condi\xE7\xF5es de quem
|
|
14422
|
+
-- pergunta \u2014 a n\xE3o ser que a condi\xE7\xE3o seja \`leakproof\`. A janela da fila
|
|
14423
|
+
-- (\`used_at >= agora - 30 dias\`) era uma EXPRESS\xC3O com \`timezone()\`, que n\xE3o \xE9
|
|
14424
|
+
-- leakproof: o \xEDndice da 013 existia e o plano o ignorava, varrendo as 116.934
|
|
14425
|
+
-- linhas e chamando \`app.has_unit\` 116.028 vezes. Medido: 7,9 s, e a tela
|
|
14426
|
+
-- morria nos 8 s do PostgREST.
|
|
14427
|
+
--
|
|
14428
|
+
-- Coluna gerada e gravada resolve pela raiz: a compara\xE7\xE3o vira \`coluna >=
|
|
14429
|
+
-- constante\`, que \xE9 leakproof, entra como condi\xE7\xE3o de \xEDndice e a RLS passa a
|
|
14430
|
+
-- rodar sobre as dezenas de linhas da janela em vez da tabela inteira. O valor
|
|
14431
|
+
-- \xE9 o mesmo de antes (o cluster roda em UTC), calculado uma vez na escrita.
|
|
14432
|
+
|
|
14433
|
+
ALTER TABLE public.plg_clinical_session_products
|
|
14434
|
+
ADD COLUMN IF NOT EXISTS used_at timestamptz
|
|
14435
|
+
GENERATED ALWAYS AS (COALESCE(timezone('UTC', executed_on::timestamp), created_at)) STORED;
|
|
14436
|
+
|
|
14437
|
+
DROP INDEX IF EXISTS public.plg_clinical_session_products_used_at_idx;
|
|
14438
|
+
CREATE INDEX IF NOT EXISTS plg_clinical_session_products_used_at_idx
|
|
14439
|
+
ON public.plg_clinical_session_products (tenant_id, used_at DESC);
|
|
14440
|
+
|
|
14441
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
14442
|
+
WITH consumed AS (
|
|
14443
|
+
SELECT m.tenant_id,
|
|
14444
|
+
m.unit_id,
|
|
14445
|
+
m.source_item_id AS item_id,
|
|
14446
|
+
m.product_id,
|
|
14447
|
+
sum(- m.quantity) AS posted_quantity,
|
|
14448
|
+
sum(m.total_cost) AS total_cost,
|
|
14449
|
+
min(m.created_at) AS used_at,
|
|
14450
|
+
min(m.movement_date) AS movement_date,
|
|
14451
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
14452
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
14453
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
14454
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
14455
|
+
FROM plg_inventory_stock_movements m
|
|
14456
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
14457
|
+
FROM plg_inventory_stock_movements r
|
|
14458
|
+
WHERE r.reverses_movement_id = m.id))
|
|
14459
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
14460
|
+
), corrected AS (
|
|
14461
|
+
SELECT c.tenant_id,
|
|
14462
|
+
c.source_item_id AS item_id,
|
|
14463
|
+
c.product_id,
|
|
14464
|
+
sum(- c.quantity) AS correction_delta,
|
|
14465
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
14466
|
+
max(c.created_at) AS corrected_at,
|
|
14467
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
14468
|
+
FROM plg_inventory_stock_movements c
|
|
14469
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
14470
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
14471
|
+
), ledger AS (
|
|
14472
|
+
SELECT c.tenant_id,
|
|
14473
|
+
c.unit_id,
|
|
14474
|
+
c.item_id,
|
|
14475
|
+
c.product_id,
|
|
14476
|
+
p.name AS product_name,
|
|
14477
|
+
p.sku AS product_sku,
|
|
14478
|
+
c.used_at,
|
|
14479
|
+
c.movement_date,
|
|
14480
|
+
c.posted_quantity,
|
|
14481
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
14482
|
+
std.standard_quantity,
|
|
14483
|
+
CASE
|
|
14484
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
14485
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
14486
|
+
END AS variance,
|
|
14487
|
+
u.abbreviation AS unit_abbreviation,
|
|
14488
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
14489
|
+
c.stock_location_id,
|
|
14490
|
+
sl.name AS stock_location_name,
|
|
14491
|
+
c.location_count,
|
|
14492
|
+
r.id AS recipe_id,
|
|
14493
|
+
r.name AS recipe_name,
|
|
14494
|
+
i.order_id,
|
|
14495
|
+
i.name AS item_name,
|
|
14496
|
+
o.reference_number AS order_reference,
|
|
14497
|
+
i.assignee_id AS performed_by_id,
|
|
14498
|
+
perf.name AS performed_by_name,
|
|
14499
|
+
o.party_id AS customer_id,
|
|
14500
|
+
cust.name AS customer_name,
|
|
14501
|
+
k.corrected_at,
|
|
14502
|
+
k.correction_reason,
|
|
14503
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
14504
|
+
'ledger'::text AS source,
|
|
14505
|
+
true AS stock_posted
|
|
14506
|
+
FROM consumed c
|
|
14507
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
14508
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
14509
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
14510
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
14511
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
14512
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
14513
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
14514
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
14515
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
14516
|
+
LEFT JOIN LATERAL ( SELECT
|
|
14517
|
+
CASE
|
|
14518
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
14519
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
14520
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
14521
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
14522
|
+
ELSE NULL::numeric
|
|
14523
|
+
END AS standard_quantity) std ON true
|
|
14524
|
+
), clinical AS (
|
|
14525
|
+
SELECT cs.tenant_id,
|
|
14526
|
+
cs.unit_id,
|
|
14527
|
+
cs.id AS item_id,
|
|
14528
|
+
cs.product_id,
|
|
14529
|
+
p.name AS product_name,
|
|
14530
|
+
p.sku AS product_sku,
|
|
14531
|
+
cs.used_at,
|
|
14532
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
14533
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
14534
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
14535
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
14536
|
+
CASE
|
|
14537
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
14538
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
14539
|
+
END AS variance,
|
|
14540
|
+
u.abbreviation AS unit_abbreviation,
|
|
14541
|
+
0::numeric AS total_cost,
|
|
14542
|
+
NULL::uuid AS stock_location_id,
|
|
14543
|
+
NULL::text AS stock_location_name,
|
|
14544
|
+
1 AS location_count,
|
|
14545
|
+
NULL::uuid AS recipe_id,
|
|
14546
|
+
NULL::text AS recipe_name,
|
|
14547
|
+
i.order_id,
|
|
14548
|
+
sv.name AS item_name,
|
|
14549
|
+
o.reference_number AS order_reference,
|
|
14550
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
14551
|
+
perf.name AS performed_by_name,
|
|
14552
|
+
s.party_id AS customer_id,
|
|
14553
|
+
cust.name AS customer_name,
|
|
14554
|
+
cs.audited_at AS corrected_at,
|
|
14555
|
+
cs.audit_notes AS correction_reason,
|
|
14556
|
+
cs.is_audited AS is_corrected,
|
|
14557
|
+
'clinical'::text AS source,
|
|
14558
|
+
false AS stock_posted
|
|
14559
|
+
FROM plg_clinical_session_products cs
|
|
14560
|
+
LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id
|
|
14561
|
+
FROM plg_clinical_sessions ss
|
|
14562
|
+
WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true
|
|
14563
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
14564
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
14565
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
14566
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
14567
|
+
FROM items it
|
|
14568
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
14569
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true
|
|
14570
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
14571
|
+
FROM orders ord
|
|
14572
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true
|
|
14573
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14574
|
+
FROM people pe
|
|
14575
|
+
WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) LIMIT 1) perf ON true
|
|
14576
|
+
LEFT JOIN LATERAL ( SELECT pe.id, pe.name
|
|
14577
|
+
FROM people pe
|
|
14578
|
+
WHERE pe.id = s.party_id LIMIT 1) cust ON true
|
|
14579
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
14580
|
+
FROM plg_inventory_stock_movements m
|
|
14581
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
14582
|
+
)
|
|
14583
|
+
SELECT tenant_id,
|
|
14584
|
+
unit_id,
|
|
14585
|
+
item_id,
|
|
14586
|
+
product_id,
|
|
14587
|
+
product_name,
|
|
14588
|
+
product_sku,
|
|
14589
|
+
used_at,
|
|
14590
|
+
movement_date,
|
|
14591
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
14592
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
14593
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
14594
|
+
variance,
|
|
14595
|
+
unit_abbreviation,
|
|
14596
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
14597
|
+
stock_location_id,
|
|
14598
|
+
stock_location_name,
|
|
14599
|
+
location_count,
|
|
14600
|
+
recipe_id,
|
|
14601
|
+
recipe_name,
|
|
14602
|
+
order_id,
|
|
14603
|
+
item_name,
|
|
14604
|
+
order_reference,
|
|
14605
|
+
performed_by_id,
|
|
14606
|
+
performed_by_name,
|
|
14607
|
+
customer_id,
|
|
14608
|
+
customer_name,
|
|
14609
|
+
corrected_at,
|
|
14610
|
+
correction_reason,
|
|
14611
|
+
is_corrected,
|
|
14612
|
+
source,
|
|
14613
|
+
stock_posted
|
|
14614
|
+
FROM ( SELECT ledger.tenant_id,
|
|
14615
|
+
ledger.unit_id,
|
|
14616
|
+
ledger.item_id,
|
|
14617
|
+
ledger.product_id,
|
|
14618
|
+
ledger.product_name,
|
|
14619
|
+
ledger.product_sku,
|
|
14620
|
+
ledger.used_at,
|
|
14621
|
+
ledger.movement_date,
|
|
14622
|
+
ledger.posted_quantity,
|
|
14623
|
+
ledger.net_quantity,
|
|
14624
|
+
ledger.standard_quantity,
|
|
14625
|
+
ledger.variance,
|
|
14626
|
+
ledger.unit_abbreviation,
|
|
14627
|
+
ledger.total_cost,
|
|
14628
|
+
ledger.stock_location_id,
|
|
14629
|
+
ledger.stock_location_name,
|
|
14630
|
+
ledger.location_count,
|
|
14631
|
+
ledger.recipe_id,
|
|
14632
|
+
ledger.recipe_name,
|
|
14633
|
+
ledger.order_id,
|
|
14634
|
+
ledger.item_name,
|
|
14635
|
+
ledger.order_reference,
|
|
14636
|
+
ledger.performed_by_id,
|
|
14637
|
+
ledger.performed_by_name,
|
|
14638
|
+
ledger.customer_id,
|
|
14639
|
+
ledger.customer_name,
|
|
14640
|
+
ledger.corrected_at,
|
|
14641
|
+
ledger.correction_reason,
|
|
14642
|
+
ledger.is_corrected,
|
|
14643
|
+
ledger.source,
|
|
14644
|
+
ledger.stock_posted
|
|
14645
|
+
FROM ledger
|
|
14646
|
+
UNION ALL
|
|
14647
|
+
SELECT clinical.tenant_id,
|
|
14648
|
+
clinical.unit_id,
|
|
14649
|
+
clinical.item_id,
|
|
14650
|
+
clinical.product_id,
|
|
14651
|
+
clinical.product_name,
|
|
14652
|
+
clinical.product_sku,
|
|
14653
|
+
clinical.used_at,
|
|
14654
|
+
clinical.movement_date,
|
|
14655
|
+
clinical.posted_quantity,
|
|
14656
|
+
clinical.net_quantity,
|
|
14657
|
+
clinical.standard_quantity,
|
|
14658
|
+
clinical.variance,
|
|
14659
|
+
clinical.unit_abbreviation,
|
|
14660
|
+
clinical.total_cost,
|
|
14661
|
+
clinical.stock_location_id,
|
|
14662
|
+
clinical.stock_location_name,
|
|
14663
|
+
clinical.location_count,
|
|
14664
|
+
clinical.recipe_id,
|
|
14665
|
+
clinical.recipe_name,
|
|
14666
|
+
clinical.order_id,
|
|
14667
|
+
clinical.item_name,
|
|
14668
|
+
clinical.order_reference,
|
|
14669
|
+
clinical.performed_by_id,
|
|
14670
|
+
clinical.performed_by_name,
|
|
14671
|
+
clinical.customer_id,
|
|
14672
|
+
clinical.customer_name,
|
|
14673
|
+
clinical.corrected_at,
|
|
14674
|
+
clinical.correction_reason,
|
|
14675
|
+
clinical.is_corrected,
|
|
14676
|
+
clinical.source,
|
|
14677
|
+
clinical.stock_posted
|
|
14678
|
+
FROM clinical) a;;
|
|
14679
|
+
`;
|
|
14680
|
+
var MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS = `-- Aprovar e baixar no estoque s\xE3o DOIS gestos, e faltavam os dois.
|
|
14681
|
+
--
|
|
14682
|
+
-- A fila da Auditoria de Uso existe desde o come\xE7o deste porte: o injetor
|
|
14683
|
+
-- lan\xE7a o que usou, a linha chega aqui, e o estoquista confere. S\xF3 que a tela
|
|
14684
|
+
-- s\xF3 sabia CORRIGIR a quantidade \u2014 n\xE3o havia como aceitar. O consumo entrava
|
|
14685
|
+
-- na fila e o saldo nunca ca\xEDa. A pergunta que a jornada 4 do V1 responde
|
|
14686
|
+
-- ("executei e o estoque n\xE3o mudou") continuava sem resposta, porque o bot\xE3o
|
|
14687
|
+
-- que a responde n\xE3o existia.
|
|
14688
|
+
--
|
|
14689
|
+
-- O V1 tem tr\xEAs a\xE7\xF5es por linha, e elas s\xE3o diferentes de prop\xF3sito:
|
|
14690
|
+
--
|
|
14691
|
+
-- \u{1F4CD} Baixar no estoque aceita E desconta do saldo
|
|
14692
|
+
-- \u270F\uFE0F Corrigir conserta a quantidade digitada errada
|
|
14693
|
+
-- \u2705 Aprovar aceita o consumo, sem mexer no saldo
|
|
14694
|
+
--
|
|
14695
|
+
-- "Aprovar" sem baixar existe para o consumo que n\xE3o sai do estoque da casa \u2014
|
|
14696
|
+
-- produto do pr\xF3prio cliente, amostra do fornecedor, item j\xE1 baixado por fora.
|
|
14697
|
+
--
|
|
14698
|
+
-- SOBRE SALDO NEGATIVO. Esta fun\xE7\xE3o N\xC3O exige saldo. O movimento \xE9 o registro
|
|
14699
|
+
-- do que foi usado numa pessoa, e isso aconteceu independentemente do que o
|
|
14700
|
+
-- sistema achava que havia na prateleira. Recusar a baixa por falta de saldo
|
|
14701
|
+
-- esconderia o consumo e deixaria a diferen\xE7a invis\xEDvel; deixar o saldo ficar
|
|
14702
|
+
-- negativo \xE9 exatamente o sinal que o estoquista precisa ver.
|
|
14703
|
+
|
|
14704
|
+
CREATE OR REPLACE FUNCTION public.inventory_approve_usage_batch(
|
|
14705
|
+
p_lines jsonb,
|
|
14706
|
+
p_post boolean DEFAULT false,
|
|
14707
|
+
p_reason text DEFAULT NULL
|
|
14708
|
+
)
|
|
14709
|
+
RETURNS jsonb
|
|
14710
|
+
LANGUAGE plpgsql
|
|
14711
|
+
SECURITY DEFINER
|
|
14712
|
+
SET search_path TO ''
|
|
14713
|
+
AS $fn$
|
|
14714
|
+
DECLARE
|
|
14715
|
+
v_tenant uuid := app.inventory_require_tenant();
|
|
14716
|
+
v_actor uuid := auth.uid();
|
|
14717
|
+
v_key text;
|
|
14718
|
+
v_op record;
|
|
14719
|
+
r record;
|
|
14720
|
+
v_cs record;
|
|
14721
|
+
v_location uuid;
|
|
14722
|
+
v_cost numeric;
|
|
14723
|
+
v_movement uuid;
|
|
14724
|
+
v_no integer := 0;
|
|
14725
|
+
v_aprovadas integer := 0;
|
|
14726
|
+
v_baixadas integer := 0;
|
|
14727
|
+
v_puladas integer := 0;
|
|
14728
|
+
v_movements jsonb := '[]'::jsonb;
|
|
14729
|
+
BEGIN
|
|
14730
|
+
PERFORM app.inventory_check_lines(p_lines);
|
|
14731
|
+
|
|
14732
|
+
SELECT 'usage_audit_approve:' || CASE WHEN p_post THEN 'post:' ELSE 'ack:' END
|
|
14733
|
+
|| md5(string_agg((l->>'item_id'), ',' ORDER BY (l->>'item_id')))
|
|
14734
|
+
INTO v_key
|
|
14735
|
+
FROM jsonb_array_elements(p_lines) AS l;
|
|
14736
|
+
|
|
14737
|
+
SELECT * INTO v_op FROM app.inventory_begin_operation(
|
|
14738
|
+
-- \`confirm\` \xE9 a opera\xE7\xE3o de quem TIRA do estoque por consumo \u2014 o mesmo
|
|
14739
|
+
-- verbo que o consumo de item de pedido usa. 'out' n\xE3o existe no cat\xE1logo
|
|
14740
|
+
-- de opera\xE7\xF5es (s\xF3 de movimentos), e o CHECK recusava.
|
|
14741
|
+
v_tenant, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,
|
|
14742
|
+
jsonb_build_object('source', 'usage_audit_approve', 'post', p_post, 'lines', p_lines));
|
|
14743
|
+
IF v_op.existing IS NOT NULL THEN
|
|
14744
|
+
RETURN v_op.existing;
|
|
14745
|
+
END IF;
|
|
14746
|
+
|
|
14747
|
+
FOR r IN
|
|
14748
|
+
SELECT (l->>'item_id')::uuid AS item_id, ord
|
|
14749
|
+
FROM jsonb_array_elements(p_lines) WITH ORDINALITY AS t(l, ord)
|
|
14750
|
+
ORDER BY ord
|
|
14751
|
+
LOOP
|
|
14752
|
+
v_no := v_no + 1;
|
|
14753
|
+
|
|
14754
|
+
SELECT cs.* INTO v_cs
|
|
14755
|
+
FROM public.plg_clinical_session_products cs
|
|
14756
|
+
WHERE cs.tenant_id = v_tenant AND cs.id = r.item_id
|
|
14757
|
+
FOR UPDATE;
|
|
14758
|
+
|
|
14759
|
+
-- Linha que n\xE3o \xE9 consumo cl\xEDnico j\xE1 nasceu como movimento do raz\xE3o: ela
|
|
14760
|
+
-- n\xE3o tem o que aprovar, e some da fila por outro caminho.
|
|
14761
|
+
IF NOT FOUND THEN
|
|
14762
|
+
v_puladas := v_puladas + 1;
|
|
14763
|
+
CONTINUE;
|
|
14764
|
+
END IF;
|
|
14765
|
+
|
|
14766
|
+
IF p_post AND coalesce(v_cs.used_quantity, 0) > 0 THEN
|
|
14767
|
+
-- ONDE A BAIXA ACONTECE. Prefer\xEAncia: o local padr\xE3o do produto na
|
|
14768
|
+
-- unidade, depois o da ficha, depois o primeiro local ativo dela.
|
|
14769
|
+
SELECT candidate.location_id INTO v_location
|
|
14770
|
+
FROM (
|
|
14771
|
+
SELECT s.default_location_id AS location_id,
|
|
14772
|
+
CASE WHEN v_cs.unit_id IS NOT NULL AND s.unit_id = v_cs.unit_id THEN 1 ELSE 2 END AS priority
|
|
14773
|
+
FROM public.plg_inventory_product_settings s
|
|
14774
|
+
WHERE s.tenant_id = v_tenant
|
|
14775
|
+
AND s.product_id = v_cs.product_id
|
|
14776
|
+
AND s.default_location_id IS NOT NULL
|
|
14777
|
+
AND (s.unit_id IS NULL OR s.unit_id = v_cs.unit_id)
|
|
14778
|
+
UNION ALL
|
|
14779
|
+
SELECT d.default_location_id, 3
|
|
14780
|
+
FROM public.plg_inventory_product_details d
|
|
14781
|
+
WHERE d.tenant_id = v_tenant
|
|
14782
|
+
AND d.product_id = v_cs.product_id
|
|
14783
|
+
AND d.default_location_id IS NOT NULL
|
|
14784
|
+
UNION ALL
|
|
14785
|
+
SELECT l.id, 4
|
|
14786
|
+
FROM public.plg_inventory_stock_locations l
|
|
14787
|
+
WHERE l.tenant_id = v_tenant AND l.is_active
|
|
14788
|
+
) candidate
|
|
14789
|
+
JOIN public.plg_inventory_stock_locations l
|
|
14790
|
+
ON l.id = candidate.location_id AND l.tenant_id = v_tenant AND l.is_active
|
|
14791
|
+
WHERE v_cs.unit_id IS NULL OR l.unit_id = v_cs.unit_id OR l.unit_id IS NULL
|
|
14792
|
+
ORDER BY candidate.priority, l.created_at, l.id
|
|
14793
|
+
LIMIT 1;
|
|
14794
|
+
|
|
14795
|
+
IF v_location IS NULL THEN
|
|
14796
|
+
RAISE EXCEPTION 'inventory: nenhum local de estoque ativo para a unidade deste consumo'
|
|
14797
|
+
USING ERRCODE = '22023';
|
|
14798
|
+
END IF;
|
|
14799
|
+
|
|
14800
|
+
SELECT p.unit_cost INTO v_cost
|
|
14801
|
+
FROM public.plg_inventory_stock_positions p
|
|
14802
|
+
WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id
|
|
14803
|
+
AND p.stock_location_id = v_location
|
|
14804
|
+
ORDER BY p.expiration_date NULLS LAST, p.created_at
|
|
14805
|
+
LIMIT 1;
|
|
14806
|
+
|
|
14807
|
+
INSERT INTO public.plg_inventory_stock_movements
|
|
14808
|
+
(tenant_id, unit_id, product_id, kind, quantity, unit_cost, source_location_id,
|
|
14809
|
+
measurement_unit_id, document_type, document_number,
|
|
14810
|
+
source_item_type, source_item_id, idempotency_key, line_no, operation_id,
|
|
14811
|
+
user_id, metadata)
|
|
14812
|
+
VALUES
|
|
14813
|
+
(v_tenant, v_cs.unit_id, v_cs.product_id, 'out', -v_cs.used_quantity,
|
|
14814
|
+
coalesce(v_cost, 0), v_location, v_cs.used_unit_id, 'consumption',
|
|
14815
|
+
coalesce(v_cs.session_id::text, v_cs.id::text),
|
|
14816
|
+
'usage_audit', v_cs.id, v_key, v_no, v_op.op_id, v_actor,
|
|
14817
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
14818
|
+
'source', 'usage_audit_approve',
|
|
14819
|
+
'session_id', v_cs.session_id,
|
|
14820
|
+
'order_item_id', v_cs.order_item_id,
|
|
14821
|
+
'reason', nullif(btrim(p_reason), ''))))
|
|
14822
|
+
RETURNING id INTO v_movement;
|
|
14823
|
+
|
|
14824
|
+
v_movements := v_movements || app.inventory_movement_json(v_movement);
|
|
14825
|
+
v_baixadas := v_baixadas + 1;
|
|
14826
|
+
END IF;
|
|
14827
|
+
|
|
14828
|
+
-- CONFERIDO \xE9 o mesmo carimbo nos dois gestos: o que sai da fila \xE9 o que
|
|
14829
|
+
-- algu\xE9m olhou, tenha ou n\xE3o baixado.
|
|
14830
|
+
UPDATE public.plg_clinical_session_products cs
|
|
14831
|
+
SET is_audited = true,
|
|
14832
|
+
audited_at = now(),
|
|
14833
|
+
audited_by = coalesce(cs.audited_by, v_actor),
|
|
14834
|
+
audit_notes = coalesce(nullif(btrim(p_reason), ''), cs.audit_notes),
|
|
14835
|
+
updated_at = now()
|
|
14836
|
+
WHERE cs.id = v_cs.id;
|
|
14837
|
+
v_aprovadas := v_aprovadas + 1;
|
|
14838
|
+
END LOOP;
|
|
14839
|
+
|
|
14840
|
+
RETURN app.inventory_finish_operation(
|
|
14841
|
+
v_tenant, v_op.op_id, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,
|
|
14842
|
+
jsonb_build_object(
|
|
14843
|
+
'operation_id', v_op.op_id, 'kind', 'usage_audit_approve',
|
|
14844
|
+
'idempotency_key', v_key, 'status', CASE WHEN p_post THEN 'posted' ELSE 'approved' END,
|
|
14845
|
+
'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,
|
|
14846
|
+
'lines_skipped', v_puladas, 'movements', v_movements),
|
|
14847
|
+
jsonb_build_object('source', 'usage_audit_approve', 'post', p_post,
|
|
14848
|
+
'lines_approved', v_aprovadas, 'lines_posted', v_baixadas));
|
|
14849
|
+
END $fn$;
|
|
14850
|
+
|
|
14851
|
+
GRANT EXECUTE ON FUNCTION public.inventory_approve_usage_batch(jsonb, boolean, text) TO authenticated;
|
|
14852
|
+
`;
|
|
14853
|
+
var MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA = `-- O NOME N\xC3O SAI DA VISTA. A vista devolve o id; quem pinta busca o nome.
|
|
14854
|
+
--
|
|
14855
|
+
-- \`people\` tem 120.379 linhas neste tenant e uma pol\xEDtica de RLS que passa por
|
|
14856
|
+
-- \`resource_grants\`. Buscar o nome do profissional e o da cliente DENTRO da
|
|
14857
|
+
-- vista fazia essa pol\xEDtica rodar linha a linha: a mesma consulta que custa
|
|
14858
|
+
-- 9 ms com chave de servi\xE7o levava **30 segundos** com a sess\xE3o de quem usa a
|
|
14859
|
+
-- tela, e o PostgREST desistia aos 8.
|
|
14860
|
+
--
|
|
14861
|
+
-- Nenhum \xEDndice conserta isso, porque o problema n\xE3o \xE9 achar a pessoa \u2014 \xE9 a
|
|
14862
|
+
-- pol\xEDtica ser avaliada uma vez por candidato. A sa\xEDda \xE9 n\xE3o perguntar ali: a
|
|
14863
|
+
-- vista entrega \`performed_by_id\` e \`customer_id\`, e a tela busca os ~50 nomes
|
|
14864
|
+
-- de uma vez, numa consulta s\xF3, com a RLS rodando uma vez para todos.
|
|
14865
|
+
|
|
14866
|
+
CREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS
|
|
14867
|
+
WITH consumed AS (
|
|
14868
|
+
SELECT m.tenant_id,
|
|
14869
|
+
m.unit_id,
|
|
14870
|
+
m.source_item_id AS item_id,
|
|
14871
|
+
m.product_id,
|
|
14872
|
+
sum(- m.quantity) AS posted_quantity,
|
|
14873
|
+
sum(m.total_cost) AS total_cost,
|
|
14874
|
+
min(m.created_at) AS used_at,
|
|
14875
|
+
min(m.movement_date) AS movement_date,
|
|
14876
|
+
(array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,
|
|
14877
|
+
count(DISTINCT m.source_location_id)::integer AS location_count,
|
|
14878
|
+
(array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,
|
|
14879
|
+
(array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id
|
|
14880
|
+
FROM plg_inventory_stock_movements m
|
|
14881
|
+
WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1
|
|
14882
|
+
FROM plg_inventory_stock_movements r
|
|
14883
|
+
WHERE r.reverses_movement_id = m.id))
|
|
14884
|
+
GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id
|
|
14885
|
+
), corrected AS (
|
|
14886
|
+
SELECT c.tenant_id,
|
|
14887
|
+
c.source_item_id AS item_id,
|
|
14888
|
+
c.product_id,
|
|
14889
|
+
sum(- c.quantity) AS correction_delta,
|
|
14890
|
+
sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,
|
|
14891
|
+
max(c.created_at) AS corrected_at,
|
|
14892
|
+
(array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason
|
|
14893
|
+
FROM plg_inventory_stock_movements c
|
|
14894
|
+
WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL
|
|
14895
|
+
GROUP BY c.tenant_id, c.source_item_id, c.product_id
|
|
14896
|
+
), ledger AS (
|
|
14897
|
+
SELECT c.tenant_id,
|
|
14898
|
+
c.unit_id,
|
|
14899
|
+
c.item_id,
|
|
14900
|
+
c.product_id,
|
|
14901
|
+
p.name AS product_name,
|
|
14902
|
+
p.sku AS product_sku,
|
|
14903
|
+
c.used_at,
|
|
14904
|
+
c.movement_date,
|
|
14905
|
+
c.posted_quantity,
|
|
14906
|
+
c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,
|
|
14907
|
+
std.standard_quantity,
|
|
14908
|
+
CASE
|
|
14909
|
+
WHEN std.standard_quantity IS NULL THEN NULL::numeric
|
|
14910
|
+
ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity
|
|
14911
|
+
END AS variance,
|
|
14912
|
+
u.abbreviation AS unit_abbreviation,
|
|
14913
|
+
c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,
|
|
14914
|
+
c.stock_location_id,
|
|
14915
|
+
sl.name AS stock_location_name,
|
|
14916
|
+
c.location_count,
|
|
14917
|
+
r.id AS recipe_id,
|
|
14918
|
+
r.name AS recipe_name,
|
|
14919
|
+
i.order_id,
|
|
14920
|
+
i.name AS item_name,
|
|
14921
|
+
o.reference_number AS order_reference,
|
|
14922
|
+
i.assignee_id AS performed_by_id,
|
|
14923
|
+
NULL::text AS performed_by_name,
|
|
14924
|
+
o.party_id AS customer_id,
|
|
14925
|
+
NULL::text AS customer_name,
|
|
14926
|
+
k.corrected_at,
|
|
14927
|
+
k.correction_reason,
|
|
14928
|
+
k.item_id IS NOT NULL AS is_corrected,
|
|
14929
|
+
'ledger'::text AS source,
|
|
14930
|
+
true AS stock_posted
|
|
14931
|
+
FROM consumed c
|
|
14932
|
+
LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id
|
|
14933
|
+
LEFT JOIN products p ON p.id = c.product_id
|
|
14934
|
+
LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id
|
|
14935
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id
|
|
14936
|
+
LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id
|
|
14937
|
+
LEFT JOIN orders o ON o.id = i.order_id
|
|
14938
|
+
LEFT JOIN people perf ON perf.id = i.assignee_id
|
|
14939
|
+
LEFT JOIN people cust ON cust.id = o.party_id
|
|
14940
|
+
LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id
|
|
14941
|
+
LEFT JOIN LATERAL ( SELECT
|
|
14942
|
+
CASE
|
|
14943
|
+
WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round
|
|
14944
|
+
FROM plg_inventory_recipe_ingredients ri
|
|
14945
|
+
WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)
|
|
14946
|
+
WHEN r.id IS NULL THEN i.quantity
|
|
14947
|
+
ELSE NULL::numeric
|
|
14948
|
+
END AS standard_quantity) std ON true
|
|
14949
|
+
), clinical AS (
|
|
14950
|
+
SELECT cs.tenant_id,
|
|
14951
|
+
cs.unit_id,
|
|
14952
|
+
cs.id AS item_id,
|
|
14953
|
+
cs.product_id,
|
|
14954
|
+
p.name AS product_name,
|
|
14955
|
+
p.sku AS product_sku,
|
|
14956
|
+
cs.used_at,
|
|
14957
|
+
COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,
|
|
14958
|
+
COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,
|
|
14959
|
+
COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,
|
|
14960
|
+
NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,
|
|
14961
|
+
CASE
|
|
14962
|
+
WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric
|
|
14963
|
+
ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity
|
|
14964
|
+
END AS variance,
|
|
14965
|
+
u.abbreviation AS unit_abbreviation,
|
|
14966
|
+
0::numeric AS total_cost,
|
|
14967
|
+
NULL::uuid AS stock_location_id,
|
|
14968
|
+
NULL::text AS stock_location_name,
|
|
14969
|
+
1 AS location_count,
|
|
14970
|
+
NULL::uuid AS recipe_id,
|
|
14971
|
+
NULL::text AS recipe_name,
|
|
14972
|
+
i.order_id,
|
|
14973
|
+
sv.name AS item_name,
|
|
14974
|
+
o.reference_number AS order_reference,
|
|
14975
|
+
COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,
|
|
14976
|
+
NULL::text AS performed_by_name,
|
|
14977
|
+
s.party_id AS customer_id,
|
|
14978
|
+
NULL::text AS customer_name,
|
|
14979
|
+
cs.audited_at AS corrected_at,
|
|
14980
|
+
cs.audit_notes AS correction_reason,
|
|
14981
|
+
cs.is_audited AS is_corrected,
|
|
14982
|
+
'clinical'::text AS source,
|
|
14983
|
+
false AS stock_posted
|
|
14984
|
+
FROM plg_clinical_session_products cs
|
|
14985
|
+
LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id
|
|
14986
|
+
FROM plg_clinical_sessions ss
|
|
14987
|
+
WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true
|
|
14988
|
+
LEFT JOIN products p ON p.id = cs.product_id
|
|
14989
|
+
LEFT JOIN products sv ON sv.id = s.service_id
|
|
14990
|
+
LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id
|
|
14991
|
+
LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id
|
|
14992
|
+
FROM items it
|
|
14993
|
+
WHERE cs.order_item_id IS NOT NULL
|
|
14994
|
+
AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true
|
|
14995
|
+
LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id
|
|
14996
|
+
FROM orders ord
|
|
14997
|
+
WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true
|
|
14998
|
+
WHERE NOT (EXISTS ( SELECT 1
|
|
14999
|
+
FROM plg_inventory_stock_movements m
|
|
15000
|
+
WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))
|
|
15001
|
+
)
|
|
15002
|
+
SELECT tenant_id,
|
|
15003
|
+
unit_id,
|
|
15004
|
+
item_id,
|
|
15005
|
+
product_id,
|
|
15006
|
+
product_name,
|
|
15007
|
+
product_sku,
|
|
15008
|
+
used_at,
|
|
15009
|
+
movement_date,
|
|
15010
|
+
posted_quantity::numeric(14,4) AS posted_quantity,
|
|
15011
|
+
net_quantity::numeric(14,4) AS net_quantity,
|
|
15012
|
+
standard_quantity::numeric(14,4) AS standard_quantity,
|
|
15013
|
+
variance,
|
|
15014
|
+
unit_abbreviation,
|
|
15015
|
+
total_cost::numeric(16,4) AS total_cost,
|
|
15016
|
+
stock_location_id,
|
|
15017
|
+
stock_location_name,
|
|
15018
|
+
location_count,
|
|
15019
|
+
recipe_id,
|
|
15020
|
+
recipe_name,
|
|
15021
|
+
order_id,
|
|
15022
|
+
item_name,
|
|
15023
|
+
order_reference,
|
|
15024
|
+
performed_by_id,
|
|
15025
|
+
performed_by_name,
|
|
15026
|
+
customer_id,
|
|
15027
|
+
customer_name,
|
|
15028
|
+
corrected_at,
|
|
15029
|
+
correction_reason,
|
|
15030
|
+
is_corrected,
|
|
15031
|
+
source,
|
|
15032
|
+
stock_posted
|
|
15033
|
+
FROM ( SELECT ledger.tenant_id,
|
|
15034
|
+
ledger.unit_id,
|
|
15035
|
+
ledger.item_id,
|
|
15036
|
+
ledger.product_id,
|
|
15037
|
+
ledger.product_name,
|
|
15038
|
+
ledger.product_sku,
|
|
15039
|
+
ledger.used_at,
|
|
15040
|
+
ledger.movement_date,
|
|
15041
|
+
ledger.posted_quantity,
|
|
15042
|
+
ledger.net_quantity,
|
|
15043
|
+
ledger.standard_quantity,
|
|
15044
|
+
ledger.variance,
|
|
15045
|
+
ledger.unit_abbreviation,
|
|
15046
|
+
ledger.total_cost,
|
|
15047
|
+
ledger.stock_location_id,
|
|
15048
|
+
ledger.stock_location_name,
|
|
15049
|
+
ledger.location_count,
|
|
15050
|
+
ledger.recipe_id,
|
|
15051
|
+
ledger.recipe_name,
|
|
15052
|
+
ledger.order_id,
|
|
15053
|
+
ledger.item_name,
|
|
15054
|
+
ledger.order_reference,
|
|
15055
|
+
ledger.performed_by_id,
|
|
15056
|
+
ledger.performed_by_name,
|
|
15057
|
+
ledger.customer_id,
|
|
15058
|
+
ledger.customer_name,
|
|
15059
|
+
ledger.corrected_at,
|
|
15060
|
+
ledger.correction_reason,
|
|
15061
|
+
ledger.is_corrected,
|
|
15062
|
+
ledger.source,
|
|
15063
|
+
ledger.stock_posted
|
|
15064
|
+
FROM ledger
|
|
15065
|
+
UNION ALL
|
|
15066
|
+
SELECT clinical.tenant_id,
|
|
15067
|
+
clinical.unit_id,
|
|
15068
|
+
clinical.item_id,
|
|
15069
|
+
clinical.product_id,
|
|
15070
|
+
clinical.product_name,
|
|
15071
|
+
clinical.product_sku,
|
|
15072
|
+
clinical.used_at,
|
|
15073
|
+
clinical.movement_date,
|
|
15074
|
+
clinical.posted_quantity,
|
|
15075
|
+
clinical.net_quantity,
|
|
15076
|
+
clinical.standard_quantity,
|
|
15077
|
+
clinical.variance,
|
|
15078
|
+
clinical.unit_abbreviation,
|
|
15079
|
+
clinical.total_cost,
|
|
15080
|
+
clinical.stock_location_id,
|
|
15081
|
+
clinical.stock_location_name,
|
|
15082
|
+
clinical.location_count,
|
|
15083
|
+
clinical.recipe_id,
|
|
15084
|
+
clinical.recipe_name,
|
|
15085
|
+
clinical.order_id,
|
|
15086
|
+
clinical.item_name,
|
|
15087
|
+
clinical.order_reference,
|
|
15088
|
+
clinical.performed_by_id,
|
|
15089
|
+
clinical.performed_by_name,
|
|
15090
|
+
clinical.customer_id,
|
|
15091
|
+
clinical.customer_name,
|
|
15092
|
+
clinical.corrected_at,
|
|
15093
|
+
clinical.correction_reason,
|
|
15094
|
+
clinical.is_corrected,
|
|
15095
|
+
clinical.source,
|
|
15096
|
+
clinical.stock_posted
|
|
15097
|
+
FROM clinical) a;;
|
|
15098
|
+
`;
|
|
15099
|
+
var MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA = `-- Sem saldo n\xE3o se baixa \u2014 e a fila diz qual produto faltou.
|
|
15100
|
+
--
|
|
15101
|
+
-- A 017 assumiu que a baixa sempre cabe. N\xE3o cabe: o gatilho de posi\xE7\xE3o da
|
|
15102
|
+
-- plataforma recusa sa\xEDda sem lastro, e o estoque deste tenant est\xE1 zerado em
|
|
15103
|
+
-- 223 dos 225 produtos. O clique voltava com o erro cru do Postgres
|
|
15104
|
+
-- ("insufficient stock (no position for product \u2026)") e a linha sumia do
|
|
15105
|
+
-- alcance de quem tentou.
|
|
15106
|
+
--
|
|
15107
|
+
-- Agora a linha sem saldo \xE9 SEPARADA: n\xE3o \xE9 baixada, n\xE3o \xE9 carimbada como
|
|
15108
|
+
-- conferida \u2014 continua pendente, que \xE9 o estado certo enquanto a entrada n\xE3o
|
|
15109
|
+
-- for dada \u2014, e volta com nome de produto, quanto precisa e quanto h\xE1.
|
|
15110
|
+
|
|
15111
|
+
CREATE OR REPLACE FUNCTION public.inventory_approve_usage_batch(
|
|
15112
|
+
p_lines jsonb,
|
|
15113
|
+
p_post boolean DEFAULT false,
|
|
15114
|
+
p_reason text DEFAULT NULL
|
|
15115
|
+
)
|
|
15116
|
+
RETURNS jsonb
|
|
15117
|
+
LANGUAGE plpgsql
|
|
15118
|
+
SECURITY DEFINER
|
|
15119
|
+
SET search_path TO ''
|
|
15120
|
+
AS $fn$
|
|
15121
|
+
DECLARE
|
|
15122
|
+
v_tenant uuid := app.inventory_require_tenant();
|
|
15123
|
+
v_actor uuid := auth.uid();
|
|
15124
|
+
v_key text;
|
|
15125
|
+
v_op record;
|
|
15126
|
+
r record;
|
|
15127
|
+
v_cs record;
|
|
15128
|
+
v_location uuid;
|
|
15129
|
+
v_cost numeric;
|
|
15130
|
+
v_movement uuid;
|
|
15131
|
+
v_no integer := 0;
|
|
15132
|
+
v_aprovadas integer := 0;
|
|
15133
|
+
v_baixadas integer := 0;
|
|
15134
|
+
v_puladas integer := 0;
|
|
15135
|
+
v_sem_saldo integer := 0;
|
|
15136
|
+
v_faltando jsonb := '[]'::jsonb;
|
|
15137
|
+
v_disp numeric;
|
|
15138
|
+
v_movements jsonb := '[]'::jsonb;
|
|
15139
|
+
BEGIN
|
|
15140
|
+
PERFORM app.inventory_check_lines(p_lines);
|
|
15141
|
+
|
|
15142
|
+
SELECT 'usage_audit_approve:' || CASE WHEN p_post THEN 'post:' ELSE 'ack:' END
|
|
15143
|
+
|| md5(string_agg((l->>'item_id'), ',' ORDER BY (l->>'item_id')))
|
|
15144
|
+
INTO v_key
|
|
15145
|
+
FROM jsonb_array_elements(p_lines) AS l;
|
|
15146
|
+
|
|
15147
|
+
SELECT * INTO v_op FROM app.inventory_begin_operation(
|
|
15148
|
+
-- \`confirm\` \xE9 a opera\xE7\xE3o de quem TIRA do estoque por consumo \u2014 o mesmo
|
|
15149
|
+
-- verbo que o consumo de item de pedido usa. 'out' n\xE3o existe no cat\xE1logo
|
|
15150
|
+
-- de opera\xE7\xF5es (s\xF3 de movimentos), e o CHECK recusava.
|
|
15151
|
+
v_tenant, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,
|
|
15152
|
+
jsonb_build_object('source', 'usage_audit_approve', 'post', p_post, 'lines', p_lines));
|
|
15153
|
+
IF v_op.existing IS NOT NULL THEN
|
|
15154
|
+
RETURN v_op.existing;
|
|
15155
|
+
END IF;
|
|
15156
|
+
|
|
15157
|
+
FOR r IN
|
|
15158
|
+
SELECT (l->>'item_id')::uuid AS item_id, ord
|
|
15159
|
+
FROM jsonb_array_elements(p_lines) WITH ORDINALITY AS t(l, ord)
|
|
15160
|
+
ORDER BY ord
|
|
15161
|
+
LOOP
|
|
15162
|
+
v_no := v_no + 1;
|
|
15163
|
+
|
|
15164
|
+
SELECT cs.* INTO v_cs
|
|
15165
|
+
FROM public.plg_clinical_session_products cs
|
|
15166
|
+
WHERE cs.tenant_id = v_tenant AND cs.id = r.item_id
|
|
15167
|
+
FOR UPDATE;
|
|
15168
|
+
|
|
15169
|
+
-- Linha que n\xE3o \xE9 consumo cl\xEDnico j\xE1 nasceu como movimento do raz\xE3o: ela
|
|
15170
|
+
-- n\xE3o tem o que aprovar, e some da fila por outro caminho.
|
|
15171
|
+
IF NOT FOUND THEN
|
|
15172
|
+
v_puladas := v_puladas + 1;
|
|
15173
|
+
CONTINUE;
|
|
15174
|
+
END IF;
|
|
15175
|
+
|
|
15176
|
+
IF p_post AND coalesce(v_cs.used_quantity, 0) > 0 THEN
|
|
15177
|
+
-- ONDE A BAIXA ACONTECE. Prefer\xEAncia: o local padr\xE3o do produto na
|
|
15178
|
+
-- unidade, depois o da ficha, depois o primeiro local ativo dela.
|
|
15179
|
+
SELECT candidate.location_id INTO v_location
|
|
15180
|
+
FROM (
|
|
15181
|
+
SELECT s.default_location_id AS location_id,
|
|
15182
|
+
CASE WHEN v_cs.unit_id IS NOT NULL AND s.unit_id = v_cs.unit_id THEN 1 ELSE 2 END AS priority
|
|
15183
|
+
FROM public.plg_inventory_product_settings s
|
|
15184
|
+
WHERE s.tenant_id = v_tenant
|
|
15185
|
+
AND s.product_id = v_cs.product_id
|
|
15186
|
+
AND s.default_location_id IS NOT NULL
|
|
15187
|
+
AND (s.unit_id IS NULL OR s.unit_id = v_cs.unit_id)
|
|
15188
|
+
UNION ALL
|
|
15189
|
+
SELECT d.default_location_id, 3
|
|
15190
|
+
FROM public.plg_inventory_product_details d
|
|
15191
|
+
WHERE d.tenant_id = v_tenant
|
|
15192
|
+
AND d.product_id = v_cs.product_id
|
|
15193
|
+
AND d.default_location_id IS NOT NULL
|
|
15194
|
+
UNION ALL
|
|
15195
|
+
SELECT l.id, 4
|
|
15196
|
+
FROM public.plg_inventory_stock_locations l
|
|
15197
|
+
WHERE l.tenant_id = v_tenant AND l.is_active
|
|
15198
|
+
) candidate
|
|
15199
|
+
JOIN public.plg_inventory_stock_locations l
|
|
15200
|
+
ON l.id = candidate.location_id AND l.tenant_id = v_tenant AND l.is_active
|
|
15201
|
+
WHERE v_cs.unit_id IS NULL OR l.unit_id = v_cs.unit_id OR l.unit_id IS NULL
|
|
15202
|
+
ORDER BY candidate.priority, l.created_at, l.id
|
|
15203
|
+
LIMIT 1;
|
|
15204
|
+
|
|
15205
|
+
IF v_location IS NULL THEN
|
|
15206
|
+
RAISE EXCEPTION 'inventory: nenhum local de estoque ativo para a unidade deste consumo'
|
|
15207
|
+
USING ERRCODE = '22023';
|
|
15208
|
+
END IF;
|
|
15209
|
+
|
|
15210
|
+
-- SEM SALDO N\xC3O SE BAIXA, e quem clicou precisa saber por qu\xEA.
|
|
15211
|
+
--
|
|
15212
|
+
-- A plataforma n\xE3o deixa o saldo ficar negativo: o gatilho de posi\xE7\xE3o
|
|
15213
|
+
-- recusa a sa\xEDda sem lastro. Em vez de devolver o erro cru do banco, a
|
|
15214
|
+
-- linha \xE9 separada e volta nomeada \u2014 a fila mant\xE9m o consumo pendente,
|
|
15215
|
+
-- que \xE9 o estado certo enquanto a entrada n\xE3o for dada.
|
|
15216
|
+
SELECT coalesce(sum(p.quantity), 0) INTO v_disp
|
|
15217
|
+
FROM public.plg_inventory_stock_positions p
|
|
15218
|
+
WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id
|
|
15219
|
+
AND p.stock_location_id = v_location;
|
|
15220
|
+
|
|
15221
|
+
IF coalesce(v_disp, 0) < v_cs.used_quantity THEN
|
|
15222
|
+
v_sem_saldo := v_sem_saldo + 1;
|
|
15223
|
+
v_faltando := v_faltando || jsonb_build_array(jsonb_build_object(
|
|
15224
|
+
'item_id', v_cs.id,
|
|
15225
|
+
'product', (SELECT pr.name FROM public.products pr WHERE pr.id = v_cs.product_id),
|
|
15226
|
+
'needed', v_cs.used_quantity,
|
|
15227
|
+
'available', coalesce(v_disp, 0)));
|
|
15228
|
+
CONTINUE;
|
|
15229
|
+
END IF;
|
|
15230
|
+
|
|
15231
|
+
SELECT p.unit_cost INTO v_cost
|
|
15232
|
+
FROM public.plg_inventory_stock_positions p
|
|
15233
|
+
WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id
|
|
15234
|
+
AND p.stock_location_id = v_location
|
|
15235
|
+
ORDER BY p.expiration_date NULLS LAST, p.created_at
|
|
15236
|
+
LIMIT 1;
|
|
15237
|
+
|
|
15238
|
+
INSERT INTO public.plg_inventory_stock_movements
|
|
15239
|
+
(tenant_id, unit_id, product_id, kind, quantity, unit_cost, source_location_id,
|
|
15240
|
+
measurement_unit_id, document_type, document_number,
|
|
15241
|
+
source_item_type, source_item_id, idempotency_key, line_no, operation_id,
|
|
15242
|
+
user_id, metadata)
|
|
15243
|
+
VALUES
|
|
15244
|
+
(v_tenant, v_cs.unit_id, v_cs.product_id, 'out', -v_cs.used_quantity,
|
|
15245
|
+
coalesce(v_cost, 0), v_location, v_cs.used_unit_id, 'consumption',
|
|
15246
|
+
coalesce(v_cs.session_id::text, v_cs.id::text),
|
|
15247
|
+
'usage_audit', v_cs.id, v_key, v_no, v_op.op_id, v_actor,
|
|
15248
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
15249
|
+
'source', 'usage_audit_approve',
|
|
15250
|
+
'session_id', v_cs.session_id,
|
|
15251
|
+
'order_item_id', v_cs.order_item_id,
|
|
15252
|
+
'reason', nullif(btrim(p_reason), ''))))
|
|
15253
|
+
RETURNING id INTO v_movement;
|
|
15254
|
+
|
|
15255
|
+
v_movements := v_movements || app.inventory_movement_json(v_movement);
|
|
15256
|
+
v_baixadas := v_baixadas + 1;
|
|
15257
|
+
END IF;
|
|
15258
|
+
|
|
15259
|
+
-- CONFERIDO \xE9 o mesmo carimbo nos dois gestos: o que sai da fila \xE9 o que
|
|
15260
|
+
-- algu\xE9m olhou, tenha ou n\xE3o baixado.
|
|
15261
|
+
UPDATE public.plg_clinical_session_products cs
|
|
15262
|
+
SET is_audited = true,
|
|
15263
|
+
audited_at = now(),
|
|
15264
|
+
audited_by = coalesce(cs.audited_by, v_actor),
|
|
15265
|
+
audit_notes = coalesce(nullif(btrim(p_reason), ''), cs.audit_notes),
|
|
15266
|
+
updated_at = now()
|
|
15267
|
+
WHERE cs.id = v_cs.id;
|
|
15268
|
+
v_aprovadas := v_aprovadas + 1;
|
|
15269
|
+
END LOOP;
|
|
15270
|
+
|
|
15271
|
+
RETURN app.inventory_finish_operation(
|
|
15272
|
+
v_tenant, v_op.op_id, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,
|
|
15273
|
+
jsonb_build_object(
|
|
15274
|
+
'operation_id', v_op.op_id, 'kind', 'usage_audit_approve',
|
|
15275
|
+
'idempotency_key', v_key, 'status', CASE WHEN p_post THEN 'posted' ELSE 'approved' END,
|
|
15276
|
+
'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,
|
|
15277
|
+
'lines_skipped', v_puladas, 'lines_no_stock', v_sem_saldo,
|
|
15278
|
+
'missing', v_faltando, 'movements', v_movements),
|
|
15279
|
+
jsonb_build_object('source', 'usage_audit_approve', 'post', p_post,
|
|
15280
|
+
'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,
|
|
15281
|
+
'lines_no_stock', v_sem_saldo));
|
|
15282
|
+
END $fn$;
|
|
15283
|
+
|
|
15284
|
+
GRANT EXECUTE ON FUNCTION public.inventory_approve_usage_batch(jsonb, boolean, text) TO authenticated;
|
|
15285
|
+
`;
|
|
11384
15286
|
var MIGRATIONS = [
|
|
11385
15287
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
11386
15288
|
{ id: "001_the_usage_that_left_stock_can_be_audited", sql: MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED },
|
|
11387
15289
|
{ id: "002_a_auditoria_fecha_em_lote", sql: MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE },
|
|
11388
15290
|
{ id: "003_stock_count_closes_through_ledger", sql: MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER },
|
|
11389
15291
|
{ id: "004_a_posicao_de_estoque_atravessa", sql: MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA },
|
|
11390
|
-
{ id: "005_a_posicao_e_consequencia_do_movimento", sql: MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO }
|
|
15292
|
+
{ id: "005_a_posicao_e_consequencia_do_movimento", sql: MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO },
|
|
15293
|
+
{ id: "006_a_producao_tem_ordem_e_estacao", sql: MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO },
|
|
15294
|
+
{ id: "007_a_producao_atravessa", sql: MIGRATION_007_A_PRODUCAO_ATRAVESSA },
|
|
15295
|
+
{ id: "008_o_grupo_da_receita_tambem_e_de_uma_casa", sql: MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA },
|
|
15296
|
+
{ id: "009_a_direcao_do_movimento_nao_esta_no_numero", sql: MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO },
|
|
15297
|
+
{ id: "010_a_entrada_de_notas_atravessa", sql: MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA },
|
|
15298
|
+
{ id: "011_o_consumo_tem_duas_origens", sql: MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS },
|
|
15299
|
+
{ id: "012_o_consumo_clinico_nao_precisa_da_tabela_de_itens_inteira", sql: MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA },
|
|
15300
|
+
{ id: "013_a_fila_de_auditoria_tem_uma_janela", sql: MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA },
|
|
15301
|
+
{ id: "014_a_sessao_se_busca_uma_a_uma", sql: MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA },
|
|
15302
|
+
{ id: "015_lateral_sem_cerca_nao_e_lateral", sql: MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL },
|
|
15303
|
+
{ id: "016_a_data_do_consumo_e_uma_coluna", sql: MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA },
|
|
15304
|
+
{ id: "017_aprovar_e_baixar_sao_dois_gestos", sql: MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS },
|
|
15305
|
+
{ id: "018_o_nome_nao_sai_da_vista", sql: MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA },
|
|
15306
|
+
{ id: "019_sem_saldo_nao_se_baixa", sql: MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA }
|
|
11391
15307
|
];
|
|
11392
15308
|
|
|
11393
15309
|
// src/index.ts
|
|
@@ -11604,7 +15520,7 @@ function createInventoryPlugin(options) {
|
|
|
11604
15520
|
component: (() => {
|
|
11605
15521
|
const Tab = () => React3.createElement(PluginSettingsPanel, {
|
|
11606
15522
|
title: config.labels.pageTitle,
|
|
11607
|
-
generalSettings: React3.createElement(InventoryGeneralSettings),
|
|
15523
|
+
generalSettings: React3.createElement(InventoryGeneralSettings, { config }),
|
|
11608
15524
|
registries: inventoryRegistries,
|
|
11609
15525
|
hostPluginId: "inventory",
|
|
11610
15526
|
routeBase: "/settings/inventory"
|