@fayz-ai/plugin-inventory 0.15.5 → 0.16.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 +14 -0
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +7 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +606 -95
- 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 +4 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InventoryPage.d.ts.map +1 -1
- package/dist/views/ProductCrudForm.d.ts.map +1 -1
- package/dist/views/ProductDetailView.d.ts.map +1 -1
- package/dist/views/ProductVariantsTab.d.ts +7 -0
- package/dist/views/ProductVariantsTab.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts +5 -1
- package/dist/views/productEntity.d.ts.map +1 -1
- package/dist/views/productFichas.d.ts +1 -1
- package/dist/views/productFichas.d.ts.map +1 -1
- package/dist/views/useHasMenu.d.ts +9 -0
- package/dist/views/useHasMenu.d.ts.map +1 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React6, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ComingSoon, ModulePage, Button, FilterPanel, FilterPanelField, RequiredFilter, FilterMenuRow, SubpageHeader, SearchSelect, cn, DatePicker, InlineLink, ListView, Modal, ModalContent, Card, Input, DataTable, SegmentedControl, ConfirmDialog, Badge, DraftCommitBar, Switch, TooltipProvider, PageHeaderActions, DashboardCanvas, Thumbnail, Skeleton, CurrencyInput, ModalHeader, ModalTitle, ModalBody, Checkbox, ModalFooter, AiActionButton, ModalDescription, Tooltip, TooltipTrigger, TooltipContent, CardContent, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, StockEntryForm, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator, openPrintWindow, printDocument, createXlsxReport, KpiCard, CardHeader, CardTitle } from '@fayz-ai/ui';
|
|
3
|
-
import { createPluginContext, UNIT_DETAIL_ZONES, PluginSettingsPanel, dedup, RelatedListTab,
|
|
3
|
+
import { createPluginContext, UNIT_DETAIL_ZONES, PluginSettingsPanel, dedup, RelatedListTab, useModuleNavigation, usePluginOnboardingGate, PluginOnboardingGateSkeleton, createViewRouter, RegistryCrudView, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, useOrganizationStore, usePermissionOptional, CrudListView, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, resolveDocumentOpenerFor, useRememberedList, usePersistedState, useScrollMemory, useAgentSurface, PageActions, useOfferedPlugins, invalidateLimit, RecordPickerModal } from '@fayz-ai/admin';
|
|
4
4
|
import { recordRailFields, RECORD_GROUP, activeFacet, createSafeDataProvider, registerTranslations, tl, countByTenant, useTranslation, getActiveUnitId, createPendingCommand, emitDataChanged, getSupabaseClientOptional, getActiveTenantId, errorMessage } from '@fayz-ai/core';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, Boxes, Layers, CircleDashed, Search, ListFilter, CalendarRange, PackageCheck, ClipboardCheck, AlertTriangle, Plus, ArrowUp, ArrowDown, Sparkles, X, Upload, Building2, Utensils, Clock, Printer, Pencil, Star, Undo2, MoreVertical, PackageX } from 'lucide-react';
|
|
@@ -491,6 +491,113 @@ function ProductBarcodesTab({ item }) {
|
|
|
491
491
|
] }) })
|
|
492
492
|
);
|
|
493
493
|
}
|
|
494
|
+
function ListSkeleton() {
|
|
495
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: /* @__PURE__ */ jsx("ul", { className: "divide-y", "aria-busy": "true", children: [28, 16, 20].map((width, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
496
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4", style: { width: `${width}%` } }),
|
|
497
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
498
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }),
|
|
499
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-9 rounded-full" })
|
|
500
|
+
] }, i)) }) }) });
|
|
501
|
+
}
|
|
502
|
+
function ProductVariantsTab({ item }) {
|
|
503
|
+
const t2 = useTranslation();
|
|
504
|
+
const provider = useInventoryProvider();
|
|
505
|
+
const productId = item?.id;
|
|
506
|
+
const [rows, setRows] = useState(null);
|
|
507
|
+
const [name, setName] = useState("");
|
|
508
|
+
const [sku, setSku] = useState("");
|
|
509
|
+
const [saving, setSaving] = useState(false);
|
|
510
|
+
const load = useCallback(async () => {
|
|
511
|
+
if (!productId || !provider.getProductVariants) {
|
|
512
|
+
setRows([]);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
setRows(await provider.getProductVariants(productId));
|
|
517
|
+
} catch (error) {
|
|
518
|
+
setRows([]);
|
|
519
|
+
toast.error(t2("inventory.variants.loadFailed"), { description: String(error?.message ?? "") });
|
|
520
|
+
}
|
|
521
|
+
}, [productId, provider, t2]);
|
|
522
|
+
useEffect(() => {
|
|
523
|
+
void load();
|
|
524
|
+
}, [load]);
|
|
525
|
+
if (!provider.addProductVariant) {
|
|
526
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-8 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("inventory.variants.unavailable") }) }) });
|
|
527
|
+
}
|
|
528
|
+
async function add() {
|
|
529
|
+
const value = name.trim();
|
|
530
|
+
if (!value || !productId) return;
|
|
531
|
+
if ((rows ?? []).some((r) => r.name.trim().toLowerCase() === value.toLowerCase())) {
|
|
532
|
+
toast.error(t2("inventory.variants.duplicate"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
setSaving(true);
|
|
536
|
+
try {
|
|
537
|
+
await provider.addProductVariant(productId, { name: value, sku });
|
|
538
|
+
setName("");
|
|
539
|
+
setSku("");
|
|
540
|
+
await load();
|
|
541
|
+
} catch (error) {
|
|
542
|
+
toast.error(t2("inventory.variants.saveFailed"), { description: String(error?.message ?? "") });
|
|
543
|
+
} finally {
|
|
544
|
+
setSaving(false);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function toggle(row, active) {
|
|
548
|
+
setRows((prev) => (prev ?? []).map((r) => r.id === row.id ? { ...r, isActive: active } : r));
|
|
549
|
+
try {
|
|
550
|
+
await provider.setProductVariantActive?.(row.id, active);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
toast.error(t2("inventory.variants.saveFailed"), { description: String(error?.message ?? "") });
|
|
553
|
+
await load();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const sized = (rows ?? []).some((r) => !r.isDefault);
|
|
557
|
+
const shown = (rows ?? []).filter((r) => !r.isDefault || !sized || r.onHand !== 0);
|
|
558
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
559
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "pt-4", children: /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-3", children: [
|
|
560
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
561
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium", children: t2("inventory.variants.name") }),
|
|
562
|
+
/* @__PURE__ */ jsx(
|
|
563
|
+
Input,
|
|
564
|
+
{
|
|
565
|
+
value: name,
|
|
566
|
+
onChange: (e) => setName(e.target.value),
|
|
567
|
+
placeholder: t2("inventory.variants.namePlaceholder"),
|
|
568
|
+
onKeyDown: (e) => {
|
|
569
|
+
if (e.key === "Enter") {
|
|
570
|
+
e.preventDefault();
|
|
571
|
+
void add();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
)
|
|
576
|
+
] }),
|
|
577
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
578
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium", children: t2("inventory.variants.sku") }),
|
|
579
|
+
/* @__PURE__ */ jsx(Input, { value: sku, onChange: (e) => setSku(e.target.value), placeholder: t2("inventory.variants.skuPlaceholder") })
|
|
580
|
+
] }),
|
|
581
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-end justify-end", children: /* @__PURE__ */ jsxs(Button, { onClick: () => void add(), disabled: !name.trim() || saving, children: [
|
|
582
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
583
|
+
t2("common.add")
|
|
584
|
+
] }) })
|
|
585
|
+
] }) }) }),
|
|
586
|
+
rows === null && /* @__PURE__ */ jsx(ListSkeleton, {}),
|
|
587
|
+
rows !== null && shown.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-8 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("inventory.variants.empty") }) }) }),
|
|
588
|
+
rows !== null && shown.length > 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: /* @__PURE__ */ jsx("ul", { className: "divide-y", children: shown.map((row) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
589
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
590
|
+
/* @__PURE__ */ jsxs("p", { className: `flex items-center gap-2 text-sm font-medium ${row.isActive ? "" : "text-muted-foreground line-through"}`, children: [
|
|
591
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: row.name }),
|
|
592
|
+
row.isDefault && row.name.trim() !== t2("inventory.variants.default") && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "shrink-0", children: t2("inventory.variants.default") })
|
|
593
|
+
] }),
|
|
594
|
+
row.sku && /* @__PURE__ */ jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", children: row.sku })
|
|
595
|
+
] }),
|
|
596
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: t2("inventory.variants.onHand", { count: row.onHand.toLocaleString("pt-BR") }) }),
|
|
597
|
+
/* @__PURE__ */ jsx(Switch, { checked: row.isActive, onCheckedChange: (v) => void toggle(row, v), "aria-label": t2("common.active") })
|
|
598
|
+
] }, row.id)) }) }) })
|
|
599
|
+
] });
|
|
600
|
+
}
|
|
494
601
|
|
|
495
602
|
// src/views/productFichas.ts
|
|
496
603
|
var yes = (key3) => (vals) => vals[key3] === true;
|
|
@@ -560,7 +667,7 @@ var ASSET_STATUS_OPTIONS = [
|
|
|
560
667
|
{ value: "disposed", label: "Baixado" },
|
|
561
668
|
{ value: "lost", label: "Extraviado" }
|
|
562
669
|
];
|
|
563
|
-
function classificationFields(t2, currency) {
|
|
670
|
+
function classificationFields(t2, currency, hasRecipes = true) {
|
|
564
671
|
const flag = (key3, label, hint) => ({
|
|
565
672
|
key: key3,
|
|
566
673
|
label,
|
|
@@ -573,9 +680,9 @@ function classificationFields(t2, currency) {
|
|
|
573
680
|
return [
|
|
574
681
|
flag("isIngredient", t2("inventory.productForm.flagIngredient"), t2("inventory.productForm.flagIngredientHint")),
|
|
575
682
|
flag("isForSale", t2("inventory.productForm.flagForSale"), t2("inventory.productForm.flagForSaleHint")),
|
|
576
|
-
flag("isIntermediate", t2("inventory.productForm.flagIntermediate"), t2("inventory.productForm.flagIntermediateHint")),
|
|
683
|
+
...hasRecipes ? [flag("isIntermediate", t2("inventory.productForm.flagIntermediate"), t2("inventory.productForm.flagIntermediateHint"))] : [],
|
|
577
684
|
flag("isAsset", t2("inventory.productForm.flagAsset"), t2("inventory.productForm.flagAssetHint")),
|
|
578
|
-
flag("isProduced", t2("inventory.productForm.flagProduced"), t2("inventory.productForm.flagProducedHint")),
|
|
685
|
+
...hasRecipes ? [flag("isProduced", t2("inventory.productForm.flagProduced"), t2("inventory.productForm.flagProducedHint"))] : [],
|
|
579
686
|
flag("isResale", t2("inventory.productForm.flagResale"), t2("inventory.productForm.flagResaleHint")),
|
|
580
687
|
flag("trackByUnitLabel", t2("inventory.productForm.flagTracked"), t2("inventory.productForm.flagTrackedHint")),
|
|
581
688
|
{
|
|
@@ -985,6 +1092,7 @@ function toFichas(values) {
|
|
|
985
1092
|
for (const name of Object.keys(FICHA_KEYS)) {
|
|
986
1093
|
if (!enabled[name]) continue;
|
|
987
1094
|
const { prefix, keys } = FICHA_KEYS[name];
|
|
1095
|
+
if (!keys.some((key3) => flatKey(prefix, key3) in values)) continue;
|
|
988
1096
|
const ficha = {};
|
|
989
1097
|
for (const key3 of keys) {
|
|
990
1098
|
const raw = values[flatKey(prefix, key3)];
|
|
@@ -1068,7 +1176,7 @@ function productImage(provider) {
|
|
|
1068
1176
|
draftField: "_imageFile"
|
|
1069
1177
|
};
|
|
1070
1178
|
}
|
|
1071
|
-
function buildProductEntity(t2, productTypes, currency, onStockChanged, provider, hasFiscal = true) {
|
|
1179
|
+
function buildProductEntity(t2, productTypes, currency, onStockChanged, provider, hasFiscal = true, hasMenu = true, hasRecipes = true) {
|
|
1072
1180
|
const typeName = (pt) => pt.labelKey ? t2(pt.labelKey) : pt.label;
|
|
1073
1181
|
const typeOptions = productTypes.map((pt) => ({
|
|
1074
1182
|
label: typeName(pt),
|
|
@@ -1297,7 +1405,12 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
|
|
|
1297
1405
|
label: t2("inventory.productForm.category"),
|
|
1298
1406
|
type: "relation",
|
|
1299
1407
|
group: "organization",
|
|
1300
|
-
relation: {
|
|
1408
|
+
relation: {
|
|
1409
|
+
table: T.productCategories,
|
|
1410
|
+
labelField: "name",
|
|
1411
|
+
filter: { kind: PRODUCT_CATEGORY_KIND },
|
|
1412
|
+
create: { permission: { feature: "inventory", action: "edit" } }
|
|
1413
|
+
},
|
|
1301
1414
|
placeholder: t2("inventory.productForm.optional"),
|
|
1302
1415
|
showInTable: false
|
|
1303
1416
|
},
|
|
@@ -1336,10 +1449,10 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
|
|
|
1336
1449
|
// As fichas: cada uma nasce de um interruptor da Classificação e some
|
|
1337
1450
|
// junto com ele. A ordem aqui é a ordem das abas do V1.
|
|
1338
1451
|
...packagingFields(t2),
|
|
1339
|
-
...classificationFields(t2, currency),
|
|
1452
|
+
...classificationFields(t2, currency, hasRecipes),
|
|
1340
1453
|
...ingredientFields(t2),
|
|
1341
|
-
...saleFields(t2, currency),
|
|
1342
|
-
...intermediateFields(t2),
|
|
1454
|
+
...hasMenu ? saleFields(t2, currency) : [],
|
|
1455
|
+
...hasRecipes ? intermediateFields(t2) : [],
|
|
1343
1456
|
...assetFields(t2, currency),
|
|
1344
1457
|
...hasFiscal ? taxFields(t2) : []
|
|
1345
1458
|
];
|
|
@@ -1348,6 +1461,8 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
|
|
|
1348
1461
|
namePlural: t2("inventory.nav.products"),
|
|
1349
1462
|
icon: "Package",
|
|
1350
1463
|
displayField: "name",
|
|
1464
|
+
// General data in the main column, situation (unit, tags, active) beside it.
|
|
1465
|
+
formColumns: 2,
|
|
1351
1466
|
// MOSTRAR e TROCAR são coisas diferentes. `image` só existe quando há
|
|
1352
1467
|
// provider, porque é ele que sabe enviar o arquivo — mas sem `imageField` a
|
|
1353
1468
|
// página do produto ficava sem foto mesmo com a foto na linha, enquanto a
|
|
@@ -1374,10 +1489,9 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
|
|
|
1374
1489
|
// Fiscal · Intermediário · Patrimônio. A aba só aparece com o
|
|
1375
1490
|
// interruptor dela ligado.
|
|
1376
1491
|
ficha("ingredient"),
|
|
1377
|
-
ficha("sale"),
|
|
1378
|
-
ficha("saleChannels"),
|
|
1492
|
+
...hasMenu ? [ficha("sale"), ficha("saleChannels")] : [],
|
|
1379
1493
|
...hasFiscal ? [ficha("tax")] : [],
|
|
1380
|
-
ficha("intermediate"),
|
|
1494
|
+
...hasRecipes ? [ficha("intermediate")] : [],
|
|
1381
1495
|
ficha("asset"),
|
|
1382
1496
|
{ ...RECORD_GROUP }
|
|
1383
1497
|
];
|
|
@@ -1398,6 +1512,12 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
|
|
|
1398
1512
|
icon: "ScanLine",
|
|
1399
1513
|
component: ProductBarcodesTab
|
|
1400
1514
|
},
|
|
1515
|
+
{
|
|
1516
|
+
id: "variants",
|
|
1517
|
+
label: t2("inventory.variants.tab"),
|
|
1518
|
+
icon: "Layers",
|
|
1519
|
+
component: ProductVariantsTab
|
|
1520
|
+
},
|
|
1401
1521
|
{
|
|
1402
1522
|
id: "stock",
|
|
1403
1523
|
label: t2("inventory.productDetail.stockTab"),
|
|
@@ -1633,6 +1753,87 @@ function ProductListView({ onNew, onOpen }) {
|
|
|
1633
1753
|
)
|
|
1634
1754
|
] });
|
|
1635
1755
|
}
|
|
1756
|
+
var INVENTORY_MODULE_DEFAULTS = {
|
|
1757
|
+
recipes: true,
|
|
1758
|
+
requisitions: true,
|
|
1759
|
+
usageAudit: true,
|
|
1760
|
+
temperature: true,
|
|
1761
|
+
// Tabs another plugin lends to Inventory (invoice entry), switched off by id.
|
|
1762
|
+
hiddenViews: []
|
|
1763
|
+
};
|
|
1764
|
+
var NO_RECIPE_VERTICALS = /* @__PURE__ */ new Set(["beauty"]);
|
|
1765
|
+
function inventoryModuleDefaults(verticalId) {
|
|
1766
|
+
return NO_RECIPE_VERTICALS.has(verticalId ?? "") ? { ...INVENTORY_MODULE_DEFAULTS, recipes: false } : INVENTORY_MODULE_DEFAULTS;
|
|
1767
|
+
}
|
|
1768
|
+
function useInventoryHouseModules() {
|
|
1769
|
+
const verticalId = useOrganizationStore((state) => state.currentOrg?.verticalId);
|
|
1770
|
+
const defaults = React6.useMemo(() => inventoryModuleDefaults(verticalId), [verticalId]);
|
|
1771
|
+
return useTenantPluginSettings("inventory", defaults);
|
|
1772
|
+
}
|
|
1773
|
+
var INVENTORY_DEFAULTS = {
|
|
1774
|
+
lowStockAlerts: true,
|
|
1775
|
+
requireReason: true,
|
|
1776
|
+
autoDeduct: false,
|
|
1777
|
+
requireSku: false,
|
|
1778
|
+
allowNegative: false,
|
|
1779
|
+
lowStockEmail: false,
|
|
1780
|
+
expiryWarnings: true
|
|
1781
|
+
};
|
|
1782
|
+
function InventoryGeneralSettings({ config }) {
|
|
1783
|
+
const t2 = useTranslation();
|
|
1784
|
+
const s = useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
|
|
1785
|
+
const mods = useInventoryHouseModules();
|
|
1786
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1787
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.modules"), description: t2("inventory.settings.modulesDesc"), children: [
|
|
1788
|
+
config?.modules.recipes && /* @__PURE__ */ jsx(
|
|
1789
|
+
ToggleRow,
|
|
1790
|
+
{
|
|
1791
|
+
label: t2("inventory.settings.recipes"),
|
|
1792
|
+
description: t2("inventory.settings.recipesDesc"),
|
|
1793
|
+
checked: mods.get("recipes"),
|
|
1794
|
+
onChange: (v) => mods.set("recipes", v)
|
|
1795
|
+
}
|
|
1796
|
+
),
|
|
1797
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requisitions"), description: t2("inventory.settings.requisitionsDesc"), checked: mods.get("requisitions"), onChange: (v) => mods.set("requisitions", v) }),
|
|
1798
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.nav.usageAudit"), description: t2("inventory.settings.usageAuditDesc"), checked: mods.get("usageAudit"), onChange: (v) => mods.set("usageAudit", v) }),
|
|
1799
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.temperature"), description: t2("inventory.settings.temperatureDesc"), checked: mods.get("temperature"), onChange: (v) => mods.set("temperature", v) }),
|
|
1800
|
+
(config?.extraViews ?? []).map((extra) => {
|
|
1801
|
+
const hidden = mods.get("hiddenViews");
|
|
1802
|
+
return /* @__PURE__ */ jsx(
|
|
1803
|
+
ToggleRow,
|
|
1804
|
+
{
|
|
1805
|
+
label: extra.label,
|
|
1806
|
+
checked: !hidden.includes(extra.id),
|
|
1807
|
+
onChange: (v) => mods.set("hiddenViews", v ? hidden.filter((id) => id !== extra.id) : [...hidden, extra.id])
|
|
1808
|
+
},
|
|
1809
|
+
extra.id
|
|
1810
|
+
);
|
|
1811
|
+
})
|
|
1812
|
+
] }),
|
|
1813
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
1814
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
|
|
1815
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
|
|
1816
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
|
|
1817
|
+
] }),
|
|
1818
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
|
|
1819
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
|
|
1820
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
|
|
1821
|
+
] }),
|
|
1822
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
|
|
1823
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
|
|
1824
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
|
|
1825
|
+
] })
|
|
1826
|
+
] });
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/views/useHasMenu.ts
|
|
1830
|
+
function useHasMenu() {
|
|
1831
|
+
const offered = useOfferedPlugins();
|
|
1832
|
+
return offered.length === 0 || offered.includes("menu");
|
|
1833
|
+
}
|
|
1834
|
+
function useHasRecipes() {
|
|
1835
|
+
return useInventoryHouseModules().get("recipes");
|
|
1836
|
+
}
|
|
1636
1837
|
function ProductCrudForm({ editId, onSaved }) {
|
|
1637
1838
|
const t2 = useTranslation();
|
|
1638
1839
|
const { productTypes, currency } = useInventoryConfig();
|
|
@@ -1640,10 +1841,10 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
1640
1841
|
const createProduct = useInventoryStore((s) => s.createProduct);
|
|
1641
1842
|
const guardProducts = useLimitGuard("products");
|
|
1642
1843
|
const isEdit = !!editId;
|
|
1643
|
-
const [initialData, setInitialData] =
|
|
1644
|
-
const [headerName, setHeaderName] =
|
|
1645
|
-
const [referenceDataReady, setReferenceDataReady] =
|
|
1646
|
-
|
|
1844
|
+
const [initialData, setInitialData] = React6.useState(isEdit ? null : {});
|
|
1845
|
+
const [headerName, setHeaderName] = React6.useState("");
|
|
1846
|
+
const [referenceDataReady, setReferenceDataReady] = React6.useState(false);
|
|
1847
|
+
React6.useEffect(() => {
|
|
1647
1848
|
let cancelled = false;
|
|
1648
1849
|
(async () => {
|
|
1649
1850
|
try {
|
|
@@ -1659,7 +1860,7 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
1659
1860
|
cancelled = true;
|
|
1660
1861
|
};
|
|
1661
1862
|
}, [provider]);
|
|
1662
|
-
|
|
1863
|
+
React6.useEffect(() => {
|
|
1663
1864
|
if (!editId) return;
|
|
1664
1865
|
let cancelled = false;
|
|
1665
1866
|
(async () => {
|
|
@@ -1672,9 +1873,11 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
1672
1873
|
cancelled = true;
|
|
1673
1874
|
};
|
|
1674
1875
|
}, [editId]);
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1876
|
+
const hasMenu = useHasMenu();
|
|
1877
|
+
const hasRecipes = useHasRecipes();
|
|
1878
|
+
const entity = React6.useMemo(
|
|
1879
|
+
() => buildProductEntity(t2, productTypes, currency, void 0, provider, true, hasMenu, hasRecipes),
|
|
1880
|
+
[t2, productTypes, currency, provider, hasMenu, hasRecipes]
|
|
1678
1881
|
);
|
|
1679
1882
|
async function save(values) {
|
|
1680
1883
|
const name = String(values.name ?? "").trim();
|
|
@@ -3356,7 +3559,7 @@ function DetailSkeleton() {
|
|
|
3356
3559
|
}
|
|
3357
3560
|
function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
3358
3561
|
const t2 = useTranslation();
|
|
3359
|
-
const { currency, productTypes } = useInventoryConfig();
|
|
3562
|
+
const { currency, productTypes, productDetailTabs: contributedTabs } = useInventoryConfig();
|
|
3360
3563
|
const provider = useInventoryProvider();
|
|
3361
3564
|
const [product, setProduct] = useState(null);
|
|
3362
3565
|
const [loading, setLoading] = useState(true);
|
|
@@ -3374,9 +3577,11 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3374
3577
|
active = false;
|
|
3375
3578
|
};
|
|
3376
3579
|
}, [productId, provider, reloadKey]);
|
|
3580
|
+
const hasMenu = useHasMenu();
|
|
3581
|
+
const hasRecipes = useHasRecipes();
|
|
3377
3582
|
const entity = useMemo(
|
|
3378
|
-
() => buildProductEntity(t2, productTypes, currency, reload),
|
|
3379
|
-
[t2, productTypes, currency, reload]
|
|
3583
|
+
() => buildProductEntity(t2, productTypes, currency, reload, void 0, true, hasMenu, hasRecipes),
|
|
3584
|
+
[t2, productTypes, currency, reload, hasMenu, hasRecipes]
|
|
3380
3585
|
);
|
|
3381
3586
|
const flat = useMemo(
|
|
3382
3587
|
() => product ? flattenProduct(product) : null,
|
|
@@ -3389,15 +3594,16 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3389
3594
|
const [photoOpen, setPhotoOpen] = useState(false);
|
|
3390
3595
|
const entityForDetail = useMemo(() => {
|
|
3391
3596
|
const extra = [];
|
|
3392
|
-
if (flat?.isForSale && provider.getProductOptionGroups) {
|
|
3597
|
+
if (hasMenu && flat?.isForSale && provider.getProductOptionGroups) {
|
|
3393
3598
|
extra.push({ id: "complements", label: t2("inventory.complements.tab"), icon: "ListPlus", component: ProductComplementsTab });
|
|
3394
3599
|
}
|
|
3395
|
-
if (flat?.isProduced || initialTab === "recipe") {
|
|
3600
|
+
if (hasRecipes && (flat?.isProduced || initialTab === "recipe")) {
|
|
3396
3601
|
extra.push({ id: "recipe", label: t2("inventory.productRecipe.tab"), icon: "ChefHat", component: ProductRecipeTab, props: { onChanged: reload } });
|
|
3397
3602
|
}
|
|
3398
3603
|
const tabs = entity.detailTabs ?? [];
|
|
3399
3604
|
const stockAt = tabs.findIndex((tab) => tab.id === "stock");
|
|
3400
|
-
const
|
|
3605
|
+
const own = stockAt < 0 ? [...tabs, ...extra] : [...tabs.slice(0, stockAt), ...extra, ...tabs.slice(stockAt)];
|
|
3606
|
+
const detailTabs = [...own, ...contributedTabs];
|
|
3401
3607
|
return {
|
|
3402
3608
|
...entity,
|
|
3403
3609
|
detailTabs,
|
|
@@ -3408,7 +3614,7 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3408
3614
|
} }] : []
|
|
3409
3615
|
]
|
|
3410
3616
|
};
|
|
3411
|
-
}, [entity, flat?.isForSale, flat?.isProduced, initialTab, provider, reload, t2]);
|
|
3617
|
+
}, [entity, hasMenu, hasRecipes, flat?.isForSale, flat?.isProduced, initialTab, provider, reload, t2, contributedTabs]);
|
|
3412
3618
|
if (loading) {
|
|
3413
3619
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
3414
3620
|
/* @__PURE__ */ jsx(SubpageHeader, { title: t2("inventory.nav.products"), onBack }),
|
|
@@ -5632,7 +5838,7 @@ function parseQuantity(raw) {
|
|
|
5632
5838
|
if (!Number.isFinite(parsed) || parsed < 0) return void 0;
|
|
5633
5839
|
return parsed;
|
|
5634
5840
|
}
|
|
5635
|
-
var CountedField =
|
|
5841
|
+
var CountedField = React6.memo(function CountedField2({
|
|
5636
5842
|
entry,
|
|
5637
5843
|
value,
|
|
5638
5844
|
onChange,
|
|
@@ -6217,47 +6423,6 @@ function BulkProductEditView() {
|
|
|
6217
6423
|
}
|
|
6218
6424
|
);
|
|
6219
6425
|
}
|
|
6220
|
-
var INVENTORY_MODULE_DEFAULTS = {
|
|
6221
|
-
recipes: true
|
|
6222
|
-
};
|
|
6223
|
-
var INVENTORY_DEFAULTS = {
|
|
6224
|
-
lowStockAlerts: true,
|
|
6225
|
-
requireReason: true,
|
|
6226
|
-
autoDeduct: false,
|
|
6227
|
-
requireSku: false,
|
|
6228
|
-
allowNegative: false,
|
|
6229
|
-
lowStockEmail: false,
|
|
6230
|
-
expiryWarnings: true
|
|
6231
|
-
};
|
|
6232
|
-
function InventoryGeneralSettings({ config }) {
|
|
6233
|
-
const t2 = useTranslation();
|
|
6234
|
-
const s = useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
|
|
6235
|
-
const mods = useTenantPluginSettings("inventory", INVENTORY_MODULE_DEFAULTS);
|
|
6236
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
6237
|
-
config?.modules.recipes && /* @__PURE__ */ jsx(SettingsGroup, { title: t2("inventory.settings.modules"), description: t2("inventory.settings.modulesDesc"), children: /* @__PURE__ */ jsx(
|
|
6238
|
-
ToggleRow,
|
|
6239
|
-
{
|
|
6240
|
-
label: t2("inventory.settings.recipes"),
|
|
6241
|
-
description: t2("inventory.settings.recipesDesc"),
|
|
6242
|
-
checked: mods.get("recipes"),
|
|
6243
|
-
onChange: (v) => mods.set("recipes", v)
|
|
6244
|
-
}
|
|
6245
|
-
) }),
|
|
6246
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
6247
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
|
|
6248
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
|
|
6249
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
|
|
6250
|
-
] }),
|
|
6251
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
|
|
6252
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
|
|
6253
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
|
|
6254
|
-
] }),
|
|
6255
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
|
|
6256
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
|
|
6257
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
|
|
6258
|
-
] })
|
|
6259
|
-
] });
|
|
6260
|
-
}
|
|
6261
6426
|
var STEPS = [
|
|
6262
6427
|
{ id: "welcome", icon: Package, titleKey: "inventory.onboarding.welcome", descKey: "inventory.onboarding.description" },
|
|
6263
6428
|
{ id: "units", icon: Ruler, titleKey: "inventory.onboarding.units.title", descKey: "inventory.onboarding.units.description" },
|
|
@@ -6307,7 +6472,7 @@ function InventoryOnboarding({ onComplete }) {
|
|
|
6307
6472
|
] })
|
|
6308
6473
|
] }) });
|
|
6309
6474
|
}
|
|
6310
|
-
function buildNav(config, view, navigate, t2) {
|
|
6475
|
+
function buildNav(config, view, navigate, t2, house) {
|
|
6311
6476
|
const items = [
|
|
6312
6477
|
// Tudo o que se faz COM o catálogo mora aqui: a lista, como ela se agrupa
|
|
6313
6478
|
// (Categorias) e como se mexe em muitos de uma vez. A primeira filha
|
|
@@ -6325,7 +6490,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6325
6490
|
active: view.startsWith("products") || view === "product-categories" || view === "bulk-products",
|
|
6326
6491
|
children: [
|
|
6327
6492
|
{ id: "products-new", kind: "action", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
|
|
6328
|
-
{ id: "products-list", label: t2("inventory.nav.products"), defaultChild: true, active: view === "products-list" || view.startsWith("products-
|
|
6493
|
+
{ id: "products-list", label: t2("inventory.nav.products"), defaultChild: true, active: view === "products-list" || view.startsWith("products-") && view.includes(":") && !view.startsWith("products-recipe:"), onClick: () => navigate("products-list") },
|
|
6329
6494
|
{ id: "product-categories", label: t2("registry.product-categories"), active: view === "product-categories", onClick: () => navigate("product-categories") },
|
|
6330
6495
|
{ id: "bulk-products", label: "Altera\xE7\xE3o em massa", active: view === "bulk-products", onClick: () => navigate("bulk-products") }
|
|
6331
6496
|
]
|
|
@@ -6374,7 +6539,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6374
6539
|
// panela rende.
|
|
6375
6540
|
{ id: "transfers", label: "Transfer\xEAncias", icon: "ArrowLeftRight", active: view === "transfers", onClick: () => navigate("transfers") },
|
|
6376
6541
|
{ id: "stock-indicators", label: "Indicadores", icon: "Target", active: view === "stock-indicators", onClick: () => navigate("stock-indicators") },
|
|
6377
|
-
{ id: "requisitions", label: "Requisi\xE7\xF5es", icon: "ClipboardList", active: view === "requisitions", onClick: () => navigate("requisitions") },
|
|
6542
|
+
...house.requisitions ? [{ id: "requisitions", label: "Requisi\xE7\xF5es", icon: "ClipboardList", active: view === "requisitions", onClick: () => navigate("requisitions") }] : [],
|
|
6378
6543
|
// Fichas Técnicas sits here, after Requisições, as in V1 (pushed below
|
|
6379
6544
|
// only when the house uses recipes).
|
|
6380
6545
|
...config.modules.recipes ? [{
|
|
@@ -6392,13 +6557,13 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6392
6557
|
//
|
|
6393
6558
|
// "Nova contagem" não está na navegação: era uma AÇÃO dentro de um menu, e
|
|
6394
6559
|
// a lista de contagens já tem o "+" no cabeçalho dela.
|
|
6395
|
-
{
|
|
6560
|
+
...house.usageAudit ? [{
|
|
6396
6561
|
id: "stock-usage-audit",
|
|
6397
6562
|
label: t2("inventory.nav.usageAudit"),
|
|
6398
6563
|
icon: "ScanSearch",
|
|
6399
6564
|
active: view === "stock-usage-audit",
|
|
6400
6565
|
onClick: () => navigate("stock-usage-audit")
|
|
6401
|
-
},
|
|
6566
|
+
}] : [],
|
|
6402
6567
|
// Produção fica de fora por decisão do founder: são seis telas e um assunto
|
|
6403
6568
|
// próprio, não um buraco do estoque.
|
|
6404
6569
|
//
|
|
@@ -6413,11 +6578,12 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6413
6578
|
active: ["stock-locations", "temperature"].some((v) => view === v),
|
|
6414
6579
|
children: [
|
|
6415
6580
|
{ id: "stock-locations", label: "Locais", defaultChild: true, active: view === "stock-locations", onClick: () => navigate("stock-locations") },
|
|
6416
|
-
{ id: "temperature", label: "Temperatura", active: view === "temperature", onClick: () => navigate("temperature") }
|
|
6581
|
+
...house.temperature ? [{ id: "temperature", label: "Temperatura", active: view === "temperature", onClick: () => navigate("temperature") }] : []
|
|
6417
6582
|
]
|
|
6418
6583
|
}
|
|
6419
6584
|
];
|
|
6420
6585
|
for (const extra of config.extraViews) {
|
|
6586
|
+
if (house.hiddenViews.includes(extra.id)) continue;
|
|
6421
6587
|
const own = [extra.id, ...(extra.children ?? []).map((c) => c.id)];
|
|
6422
6588
|
const hits = (id) => view === id || view.startsWith(`${id}/`) || view.startsWith(`${id}:`);
|
|
6423
6589
|
items.push({
|
|
@@ -6441,7 +6607,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6441
6607
|
}
|
|
6442
6608
|
function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
6443
6609
|
const t2 = useTranslation();
|
|
6444
|
-
const houseModules =
|
|
6610
|
+
const houseModules = useInventoryHouseModules();
|
|
6445
6611
|
const recipesOn = houseModules.get("recipes");
|
|
6446
6612
|
const config = useMemo(
|
|
6447
6613
|
() => appConfig.modules.recipes && recipesOn ? appConfig : { ...appConfig, modules: { ...appConfig.modules, recipes: false } },
|
|
@@ -6462,15 +6628,20 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6462
6628
|
"products-recipe": 1,
|
|
6463
6629
|
settings: 1
|
|
6464
6630
|
}, "dashboard");
|
|
6465
|
-
const checkExistingData =
|
|
6631
|
+
const checkExistingData = React6.useCallback(
|
|
6466
6632
|
async () => (await provider.getProducts({ pageSize: 1 })).total > 0,
|
|
6467
6633
|
[provider]
|
|
6468
6634
|
);
|
|
6469
6635
|
const onboarding = usePluginOnboardingGate("saas-core:inventory-onboarded", checkExistingData);
|
|
6470
6636
|
const isSettings = view === "settings";
|
|
6471
6637
|
const isSummary = view === "dashboard" || view === "";
|
|
6472
|
-
const nav = buildNav(config, view, navigate, t2
|
|
6473
|
-
|
|
6638
|
+
const nav = buildNav(config, view, navigate, t2, {
|
|
6639
|
+
requisitions: houseModules.get("requisitions"),
|
|
6640
|
+
usageAudit: houseModules.get("usageAudit"),
|
|
6641
|
+
temperature: houseModules.get("temperature"),
|
|
6642
|
+
hiddenViews: houseModules.get("hiddenViews")
|
|
6643
|
+
});
|
|
6644
|
+
const completeOnboarding = React6.useCallback(async () => {
|
|
6474
6645
|
await provider.ensureReferenceData?.();
|
|
6475
6646
|
onboarding.complete();
|
|
6476
6647
|
}, [provider, onboarding.complete]);
|
|
@@ -6535,9 +6706,14 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6535
6706
|
{ id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
|
|
6536
6707
|
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate(`products-detail:${id}`) }) },
|
|
6537
6708
|
{ id: "products-detail", render: ({ id }) => /* @__PURE__ */ jsx(ProductDetailView, { productId: id, onBack: () => navigate("products-list"), onEdit: () => navigate(`products-edit:${id}`) }) },
|
|
6538
|
-
//
|
|
6539
|
-
// come back to the same page with
|
|
6540
|
-
|
|
6709
|
+
// Every tab writes `/products/<id>/<tab>` into the URL, so each of those
|
|
6710
|
+
// links has to come back to the same page with that tab open. Only
|
|
6711
|
+
// `stock` had a route: reloading on any other tab fell through to the
|
|
6712
|
+
// module's first screen.
|
|
6713
|
+
...["stock", "variants", "barcodes", "complements", ...config.productDetailTabs.map((tab) => tab.id)].map((tab) => ({
|
|
6714
|
+
id: `products-${tab}`,
|
|
6715
|
+
render: ({ id }) => /* @__PURE__ */ jsx(ProductDetailView, { productId: id, initialTab: tab, onBack: () => navigate("products-list"), onEdit: () => navigate(`products-edit:${id}`) })
|
|
6716
|
+
}))
|
|
6541
6717
|
] : [],
|
|
6542
6718
|
{ id: "stock-entry", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
|
|
6543
6719
|
{ id: "stock-exit", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
|
|
@@ -6577,14 +6753,19 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6577
6753
|
// topo e largá-lo numa fileira própria, encostado à direita do corpo. Com
|
|
6578
6754
|
// isso, Locais e Categorias eram as duas únicas listas do Estoque cujo botão
|
|
6579
6755
|
// de criar ficava em outro lugar que o de Produtos e o de Contagens.
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6756
|
+
// The module writes a view id as a path (`product-categories` is
|
|
6757
|
+
// `/product/categories`), so the registry's base path and its own
|
|
6758
|
+
// new/edit addresses have to be spelled the same way or "New" lands on 404.
|
|
6759
|
+
...["", "-new", "-edit", "-detail"].flatMap((suffix) => [
|
|
6760
|
+
{ id: `product-categories${suffix}`, render: () => {
|
|
6761
|
+
const reg = registries?.find((r) => r.id === "product-categories");
|
|
6762
|
+
return reg ? /* @__PURE__ */ jsx(RegistryCrudView, { registry: reg, basePath: "/inventory/product/categories" }) : /* @__PURE__ */ jsx(ComingSoon, { title: "Categorias", summary: "Como o cat\xE1logo se agrupa.", v1Path: "Estoque \u2192 Produtos \u2192 Categorias", missing: ["Este aplicativo n\xE3o declarou o cadastro de categorias"] });
|
|
6763
|
+
} },
|
|
6764
|
+
{ id: `stock-locations${suffix}`, render: () => {
|
|
6765
|
+
const reg = registries?.find((r) => r.id === "stock-locations");
|
|
6766
|
+
return reg ? /* @__PURE__ */ jsx(RegistryCrudView, { registry: reg, basePath: "/inventory/stock/locations" }) : /* @__PURE__ */ jsx(StockLocationsView, {});
|
|
6767
|
+
} }
|
|
6768
|
+
]),
|
|
6588
6769
|
{ id: "temperature", render: () => /* @__PURE__ */ jsx(TemperatureView, {}) },
|
|
6589
6770
|
{ id: "bulk-products", render: () => /* @__PURE__ */ jsx(BulkProductEditView, {}) },
|
|
6590
6771
|
{ id: "stock-indicators", render: () => /* @__PURE__ */ jsx(StockIndicatorsView, {}) },
|
|
@@ -8406,6 +8587,42 @@ function createSupabaseInventoryProvider() {
|
|
|
8406
8587
|
});
|
|
8407
8588
|
if (error) throw new Error(error.message);
|
|
8408
8589
|
},
|
|
8590
|
+
async getProductVariants(productId) {
|
|
8591
|
+
const { core } = getClients();
|
|
8592
|
+
const { data } = await core.from("product_variants").select("id, name, sku, is_default, is_active, sort_order").eq("product_id", productId).order("sort_order", { ascending: true }).order("created_at", { ascending: true });
|
|
8593
|
+
const rows = data ?? [];
|
|
8594
|
+
if (rows.length === 0) return [];
|
|
8595
|
+
const balances = await core.from("stock_balances").select("variant_id, on_hand").in("variant_id", rows.map((r) => r.id));
|
|
8596
|
+
const onHand = /* @__PURE__ */ new Map();
|
|
8597
|
+
for (const b of balances.data ?? []) {
|
|
8598
|
+
onHand.set(b.variant_id, (onHand.get(b.variant_id) ?? 0) + (Number(b.on_hand) || 0));
|
|
8599
|
+
}
|
|
8600
|
+
return rows.map((r) => ({
|
|
8601
|
+
id: r.id,
|
|
8602
|
+
name: r.name,
|
|
8603
|
+
sku: r.sku ?? void 0,
|
|
8604
|
+
isDefault: !!r.is_default,
|
|
8605
|
+
isActive: r.is_active !== false,
|
|
8606
|
+
onHand: onHand.get(r.id) ?? 0
|
|
8607
|
+
}));
|
|
8608
|
+
},
|
|
8609
|
+
async addProductVariant(productId, input) {
|
|
8610
|
+
const { core } = getClients();
|
|
8611
|
+
const existing = await provider.getProductVariants(productId);
|
|
8612
|
+
const { error } = await core.from("product_variants").insert({
|
|
8613
|
+
tenant_id: getTenantId(),
|
|
8614
|
+
product_id: productId,
|
|
8615
|
+
name: input.name.trim(),
|
|
8616
|
+
sku: input.sku?.trim() || null,
|
|
8617
|
+
sort_order: existing.length
|
|
8618
|
+
});
|
|
8619
|
+
if (error) throw new Error(error.message);
|
|
8620
|
+
},
|
|
8621
|
+
async setProductVariantActive(id, active) {
|
|
8622
|
+
const { core } = getClients();
|
|
8623
|
+
const { error } = await core.from("product_variants").update({ is_active: active }).eq("id", id);
|
|
8624
|
+
if (error) throw new Error(error.message);
|
|
8625
|
+
},
|
|
8409
8626
|
async removeProductBarcode(id) {
|
|
8410
8627
|
const { core } = getClients();
|
|
8411
8628
|
const { error } = await core.from(T.productBarcodes).delete().eq("id", id);
|
|
@@ -10396,6 +10613,11 @@ var en = {
|
|
|
10396
10613
|
"inventory.settings.modulesDesc": "What this house shows inside Inventory",
|
|
10397
10614
|
"inventory.settings.recipes": "Recipes",
|
|
10398
10615
|
"inventory.settings.recipesDesc": "The bill of materials that says what each product consumes",
|
|
10616
|
+
"inventory.settings.requisitions": "Requisitions",
|
|
10617
|
+
"inventory.settings.requisitionsDesc": "Internal requests for material between areas",
|
|
10618
|
+
"inventory.settings.usageAuditDesc": "What left stock against what was sold",
|
|
10619
|
+
"inventory.settings.temperature": "Temperature",
|
|
10620
|
+
"inventory.settings.temperatureDesc": "Temperature log for refrigerated locations",
|
|
10399
10621
|
"inventory.settings.stockManagement": "Stock Management",
|
|
10400
10622
|
"inventory.settings.stockManagementDesc": "How stock levels are tracked",
|
|
10401
10623
|
"inventory.settings.lowStockAlerts": "Low stock alerts",
|
|
@@ -10518,6 +10740,18 @@ var en = {
|
|
|
10518
10740
|
"inventory.productType.sale": "For sale",
|
|
10519
10741
|
"inventory.productType.intermediate": "Intermediate",
|
|
10520
10742
|
"inventory.productType.asset": "Asset",
|
|
10743
|
+
"inventory.variants.tab": "Variants",
|
|
10744
|
+
"inventory.variants.name": "Variant",
|
|
10745
|
+
"inventory.variants.namePlaceholder": "E.g. M, L, XL, Blue",
|
|
10746
|
+
"inventory.variants.sku": "SKU",
|
|
10747
|
+
"inventory.variants.skuPlaceholder": "Optional",
|
|
10748
|
+
"inventory.variants.default": "Default",
|
|
10749
|
+
"inventory.variants.empty": "No variants. The product is sold and stocked as a single item.",
|
|
10750
|
+
"inventory.variants.duplicate": "A variant with this name already exists.",
|
|
10751
|
+
"inventory.variants.loadFailed": "Could not load the variants.",
|
|
10752
|
+
"inventory.variants.saveFailed": "Could not save the variant.",
|
|
10753
|
+
"inventory.variants.unavailable": "Variants are not available in this workspace.",
|
|
10754
|
+
"inventory.variants.onHand": "{{count}} in stock",
|
|
10521
10755
|
"inventory.barcodes.tab": "Barcodes",
|
|
10522
10756
|
"inventory.barcodes.code": "Barcode",
|
|
10523
10757
|
"inventory.barcodes.codePlaceholder": "Type or scan",
|
|
@@ -11233,6 +11467,11 @@ var ptBR = {
|
|
|
11233
11467
|
"inventory.settings.modulesDesc": "O que desta casa aparece no Estoque",
|
|
11234
11468
|
"inventory.settings.recipes": "Receitas",
|
|
11235
11469
|
"inventory.settings.recipesDesc": "A ficha t\xE9cnica que diz quanto cada produto consome",
|
|
11470
|
+
"inventory.settings.requisitions": "Requisi\xE7\xF5es",
|
|
11471
|
+
"inventory.settings.requisitionsDesc": "Pedidos internos de material entre setores",
|
|
11472
|
+
"inventory.settings.usageAuditDesc": "O que saiu do estoque contra o que foi vendido",
|
|
11473
|
+
"inventory.settings.temperature": "Temperatura",
|
|
11474
|
+
"inventory.settings.temperatureDesc": "Registro de temperatura dos locais refrigerados",
|
|
11236
11475
|
"inventory.settings.stockManagement": "Gest\xE3o de Estoque",
|
|
11237
11476
|
"inventory.settings.stockManagementDesc": "Como os n\xEDveis de estoque s\xE3o rastreados",
|
|
11238
11477
|
"inventory.settings.lowStockAlerts": "Alertas de estoque baixo",
|
|
@@ -11355,6 +11594,18 @@ var ptBR = {
|
|
|
11355
11594
|
"inventory.productType.sale": "Venda",
|
|
11356
11595
|
"inventory.productType.intermediate": "Intermedi\xE1rio",
|
|
11357
11596
|
"inventory.productType.asset": "Patrim\xF4nio",
|
|
11597
|
+
"inventory.variants.tab": "Varia\xE7\xF5es",
|
|
11598
|
+
"inventory.variants.name": "Varia\xE7\xE3o",
|
|
11599
|
+
"inventory.variants.namePlaceholder": "Ex.: M, G, GG, Azul",
|
|
11600
|
+
"inventory.variants.sku": "SKU",
|
|
11601
|
+
"inventory.variants.skuPlaceholder": "Opcional",
|
|
11602
|
+
"inventory.variants.default": "Padr\xE3o",
|
|
11603
|
+
"inventory.variants.empty": "Nenhuma varia\xE7\xE3o. O produto \xE9 vendido e estocado como um item s\xF3.",
|
|
11604
|
+
"inventory.variants.duplicate": "J\xE1 existe uma varia\xE7\xE3o com esse nome.",
|
|
11605
|
+
"inventory.variants.loadFailed": "N\xE3o foi poss\xEDvel carregar as varia\xE7\xF5es.",
|
|
11606
|
+
"inventory.variants.saveFailed": "N\xE3o foi poss\xEDvel salvar a varia\xE7\xE3o.",
|
|
11607
|
+
"inventory.variants.unavailable": "Varia\xE7\xF5es n\xE3o est\xE3o dispon\xEDveis nesta conta.",
|
|
11608
|
+
"inventory.variants.onHand": "{{count}} em estoque",
|
|
11358
11609
|
"inventory.barcodes.tab": "C\xF3digos de Barras",
|
|
11359
11610
|
"inventory.barcodes.code": "C\xF3digo de barras",
|
|
11360
11611
|
"inventory.barcodes.codePlaceholder": "Digite ou bipe",
|
|
@@ -20874,6 +21125,261 @@ $$;
|
|
|
20874
21125
|
REVOKE ALL ON FUNCTION public.inventory_save_recipe(jsonb) FROM PUBLIC, anon;
|
|
20875
21126
|
GRANT EXECUTE ON FUNCTION public.inventory_save_recipe(jsonb) TO authenticated;
|
|
20876
21127
|
`;
|
|
21128
|
+
var MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY = `-- ============================================================================
|
|
21129
|
+
-- 026 \u2014 stock cost knows its currency
|
|
21130
|
+
--
|
|
21131
|
+
-- \`unit_cost\` was a bare number. With a shop in Rio and one in Barcelona, and
|
|
21132
|
+
-- shirts paid for in dollars, three currencies would have met in one weighted
|
|
21133
|
+
-- average.
|
|
21134
|
+
--
|
|
21135
|
+
-- The rule: the cost a position carries is ALWAYS in the currency of the unit
|
|
21136
|
+
-- that holds the stock, so every sum and average that already exists stays
|
|
21137
|
+
-- right. What was actually paid, when it was paid in something else, is kept
|
|
21138
|
+
-- beside it (\`source_currency\`, \`source_unit_cost\`, \`fx_rate\`) and converted
|
|
21139
|
+
-- once, on the way in.
|
|
21140
|
+
--
|
|
21141
|
+
-- A receipt says the currency on the document (or the line):
|
|
21142
|
+
-- inventory_receive(loc, lines, '{"currency":"USD","fx_rate":5.42}')
|
|
21143
|
+
-- With no rate given, the tenant's rate for the day is used; with none on
|
|
21144
|
+
-- file, the receipt is refused rather than booked at a made-up cost.
|
|
21145
|
+
--
|
|
21146
|
+
-- Moving stock between units of different currencies is refused for now: the
|
|
21147
|
+
-- same cost number would land in the other currency unconverted.
|
|
21148
|
+
-- ============================================================================
|
|
21149
|
+
|
|
21150
|
+
DO $$
|
|
21151
|
+
BEGIN
|
|
21152
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
21153
|
+
WHERE table_schema = 'public' AND table_name = 'plg_inventory_stock_movements' AND column_name = 'currency') THEN
|
|
21154
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
21155
|
+
ALTER TABLE public.plg_inventory_stock_movements ALTER COLUMN currency DROP DEFAULT;
|
|
21156
|
+
END IF;
|
|
21157
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
21158
|
+
WHERE table_schema = 'public' AND table_name = 'plg_inventory_stock_positions' AND column_name = 'currency') THEN
|
|
21159
|
+
ALTER TABLE public.plg_inventory_stock_positions ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
21160
|
+
ALTER TABLE public.plg_inventory_stock_positions ALTER COLUMN currency DROP DEFAULT;
|
|
21161
|
+
END IF;
|
|
21162
|
+
END $$;
|
|
21163
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS source_currency text;
|
|
21164
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS source_unit_cost numeric;
|
|
21165
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS fx_rate numeric;
|
|
21166
|
+
|
|
21167
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.currency IS 'Currency of unit_cost/total_cost: the unit that holds the stock.';
|
|
21168
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.source_currency IS 'Currency the goods were paid in, when not the unit''s.';
|
|
21169
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.source_unit_cost IS 'Unit cost as paid, in source_currency.';
|
|
21170
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.fx_rate IS '1 source_currency = fx_rate of the unit''s currency, as used for this entry.';
|
|
21171
|
+
COMMENT ON COLUMN public.plg_inventory_stock_positions.currency IS 'Currency of unit_cost: the unit that holds this balance.';
|
|
21172
|
+
|
|
21173
|
+
CREATE OR REPLACE FUNCTION app.inventory_movements_currency()
|
|
21174
|
+
RETURNS trigger
|
|
21175
|
+
LANGUAGE plpgsql
|
|
21176
|
+
SECURITY DEFINER
|
|
21177
|
+
SET search_path TO ''
|
|
21178
|
+
AS $function$
|
|
21179
|
+
DECLARE
|
|
21180
|
+
v_unit_cur text := app.unit_currency(NEW.tenant_id, NEW.unit_id);
|
|
21181
|
+
v_dst_unit uuid;
|
|
21182
|
+
v_src text := nullif(upper(btrim(coalesce(NEW.source_currency, ''))), '');
|
|
21183
|
+
BEGIN
|
|
21184
|
+
NEW.currency := v_unit_cur;
|
|
21185
|
+
|
|
21186
|
+
IF NEW.destination_location_id IS NOT NULL THEN
|
|
21187
|
+
SELECT l.unit_id INTO v_dst_unit FROM public.plg_inventory_stock_locations l WHERE l.id = NEW.destination_location_id;
|
|
21188
|
+
IF app.unit_currency(NEW.tenant_id, v_dst_unit) <> v_unit_cur THEN
|
|
21189
|
+
RAISE EXCEPTION 'Transfer\xEAncia entre filiais de moedas diferentes (% \u2192 %) ainda n\xE3o \xE9 suportada: d\xEA sa\xEDda em uma e entrada na outra, informando o custo.',
|
|
21190
|
+
v_unit_cur, app.unit_currency(NEW.tenant_id, v_dst_unit)
|
|
21191
|
+
USING ERRCODE = '0A000';
|
|
21192
|
+
END IF;
|
|
21193
|
+
END IF;
|
|
21194
|
+
|
|
21195
|
+
IF v_src IS NULL OR v_src = v_unit_cur THEN
|
|
21196
|
+
NEW.source_currency := NULL; NEW.source_unit_cost := NULL; NEW.fx_rate := NULL;
|
|
21197
|
+
RETURN NEW;
|
|
21198
|
+
END IF;
|
|
21199
|
+
|
|
21200
|
+
NEW.source_currency := v_src;
|
|
21201
|
+
NEW.source_unit_cost := coalesce(NEW.source_unit_cost, NEW.unit_cost, 0);
|
|
21202
|
+
-- The tenant's rate table belongs to financial, which may not be installed.
|
|
21203
|
+
IF NEW.fx_rate IS NULL AND to_regprocedure('public.plg_financial_fx_rate(uuid,text,text,date)') IS NOT NULL THEN
|
|
21204
|
+
EXECUTE 'SELECT public.plg_financial_fx_rate($1, $2, $3, $4)'
|
|
21205
|
+
INTO NEW.fx_rate USING NEW.tenant_id, v_src, v_unit_cur, NEW.movement_date;
|
|
21206
|
+
END IF;
|
|
21207
|
+
IF NEW.fx_rate IS NULL OR NEW.fx_rate <= 0 THEN
|
|
21208
|
+
RAISE EXCEPTION 'Informe a taxa de c\xE2mbio de % para % desta entrada (ou cadastre a taxa do dia).', v_src, v_unit_cur
|
|
21209
|
+
USING ERRCODE = '22023';
|
|
21210
|
+
END IF;
|
|
21211
|
+
NEW.unit_cost := round(NEW.source_unit_cost * NEW.fx_rate, 4);
|
|
21212
|
+
NEW.total_cost := abs(NEW.quantity) * NEW.unit_cost;
|
|
21213
|
+
RETURN NEW;
|
|
21214
|
+
END $function$;
|
|
21215
|
+
|
|
21216
|
+
-- Named to fire after \`plg_inventory_stock_movements_before_insert\`, which
|
|
21217
|
+
-- stamps the unit and the cost this converts.
|
|
21218
|
+
DROP TRIGGER IF EXISTS plg_inventory_stock_movements_currency ON public.plg_inventory_stock_movements;
|
|
21219
|
+
CREATE TRIGGER plg_inventory_stock_movements_currency
|
|
21220
|
+
BEFORE INSERT ON public.plg_inventory_stock_movements
|
|
21221
|
+
FOR EACH ROW EXECUTE FUNCTION app.inventory_movements_currency();
|
|
21222
|
+
|
|
21223
|
+
CREATE OR REPLACE FUNCTION app.inventory_positions_currency()
|
|
21224
|
+
RETURNS trigger
|
|
21225
|
+
LANGUAGE plpgsql
|
|
21226
|
+
SECURITY DEFINER
|
|
21227
|
+
SET search_path TO ''
|
|
21228
|
+
AS $function$
|
|
21229
|
+
DECLARE v_unit uuid;
|
|
21230
|
+
BEGIN
|
|
21231
|
+
SELECT l.unit_id INTO v_unit FROM public.plg_inventory_stock_locations l WHERE l.id = NEW.stock_location_id;
|
|
21232
|
+
NEW.currency := app.unit_currency(NEW.tenant_id, coalesce(v_unit, NEW.unit_id));
|
|
21233
|
+
RETURN NEW;
|
|
21234
|
+
END $function$;
|
|
21235
|
+
DROP TRIGGER IF EXISTS plg_inventory_stock_positions_currency ON public.plg_inventory_stock_positions;
|
|
21236
|
+
CREATE TRIGGER plg_inventory_stock_positions_currency
|
|
21237
|
+
BEFORE INSERT ON public.plg_inventory_stock_positions
|
|
21238
|
+
FOR EACH ROW EXECUTE FUNCTION app.inventory_positions_currency();
|
|
21239
|
+
|
|
21240
|
+
-- The receipt passes the document's currency through. Patched in place: the
|
|
21241
|
+
-- live body differs from the file that created it.
|
|
21242
|
+
DO $patch$
|
|
21243
|
+
DECLARE
|
|
21244
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21245
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21246
|
+
v_cols constant text := 'idempotency_key, line_no, operation_id, metadata)';
|
|
21247
|
+
v_vals constant text := 'v_key, v_no, v_op.op_id, v_ln.declared)';
|
|
21248
|
+
BEGIN
|
|
21249
|
+
IF position('source_currency' in v_def) > 0 THEN
|
|
21250
|
+
RETURN; -- already passes it (a replay, or a later rewrite)
|
|
21251
|
+
END IF;
|
|
21252
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21253
|
+
RAISE EXCEPTION '026: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21254
|
+
END IF;
|
|
21255
|
+
v_def := overlay(v_def PLACING 'idempotency_key, line_no, operation_id, metadata, source_currency, fx_rate)'
|
|
21256
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21257
|
+
v_def := overlay(v_def PLACING
|
|
21258
|
+
'v_key, v_no, v_op.op_id, v_ln.declared,' || E'\\n'
|
|
21259
|
+
|| ' nullif(upper(btrim(coalesce(v_line->>''currency'', v_doc->>''currency'', ''''))), ''''),' || E'\\n'
|
|
21260
|
+
|| ' coalesce(nullif(v_line->>''fx_rate'', '''')::numeric, nullif(v_doc->>''fx_rate'', '''')::numeric))'
|
|
21261
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21262
|
+
EXECUTE v_def;
|
|
21263
|
+
END $patch$;
|
|
21264
|
+
`;
|
|
21265
|
+
var MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = `-- ============================================================================
|
|
21266
|
+
-- 027 \u2014 a receipt names the line it came from
|
|
21267
|
+
--
|
|
21268
|
+
-- Stock received because of a bill had no tie to the bill line: the ledger
|
|
21269
|
+
-- kept \`source_item_type\` / \`source_item_id\` columns, but \`inventory_receive\`
|
|
21270
|
+
-- never filled them, so "did this line enter stock?" was answered by guessing
|
|
21271
|
+
-- from the invoice number. A line of the receipt can now say where it came
|
|
21272
|
+
-- from, and the receipt writes it down.
|
|
21273
|
+
--
|
|
21274
|
+
-- Patched in place, as 026 did: the live body differs from the files.
|
|
21275
|
+
-- ============================================================================
|
|
21276
|
+
|
|
21277
|
+
DO $patch$
|
|
21278
|
+
DECLARE
|
|
21279
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21280
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21281
|
+
v_cols constant text := 'metadata, source_currency, fx_rate)';
|
|
21282
|
+
v_vals constant text := 'nullif(v_doc->>''fx_rate'', '''')::numeric))';
|
|
21283
|
+
BEGIN
|
|
21284
|
+
IF position('source_item_id' in v_def) > 0 THEN
|
|
21285
|
+
RETURN; -- already writes it (a replay, or a later rewrite)
|
|
21286
|
+
END IF;
|
|
21287
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21288
|
+
RAISE EXCEPTION '027: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21289
|
+
END IF;
|
|
21290
|
+
v_def := overlay(v_def PLACING 'metadata, source_currency, fx_rate, source_item_type, source_item_id)'
|
|
21291
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21292
|
+
v_def := overlay(v_def PLACING
|
|
21293
|
+
'nullif(v_doc->>''fx_rate'', '''')::numeric),' || E'\\n'
|
|
21294
|
+
|| ' nullif(v_line->>''source_item_type'', ''''), nullif(v_line->>''source_item_id'', '''')::uuid)'
|
|
21295
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21296
|
+
EXECUTE v_def;
|
|
21297
|
+
END $patch$;
|
|
21298
|
+
|
|
21299
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_stock_movements_source_item_idx
|
|
21300
|
+
ON public.plg_inventory_stock_movements (tenant_id, source_item_id) WHERE source_item_id IS NOT NULL;
|
|
21301
|
+
`;
|
|
21302
|
+
var MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = `-- ============================================================================
|
|
21303
|
+
-- 028 \u2014 a receipt keeps the variant
|
|
21304
|
+
--
|
|
21305
|
+
-- \`inventory_receive\` read \`variant_id\` off each line, checked it belonged to
|
|
21306
|
+
-- the product, and then did not write it: every receipt landed on the
|
|
21307
|
+
-- product's default variant. A shirt received as 10 M and 9 G was 19 of
|
|
21308
|
+
-- "Padr\xE3o". The line's variant now reaches the ledger row, which is what the
|
|
21309
|
+
-- per-variant balance is computed from.
|
|
21310
|
+
--
|
|
21311
|
+
-- Patched in place, as 026 and 027 did.
|
|
21312
|
+
--
|
|
21313
|
+
-- The column itself was never in this plugin's chain (022 says so): pools got
|
|
21314
|
+
-- it by hand, a database built from zero did not. It is declared here, in the
|
|
21315
|
+
-- shape the pools have. The value is read off the line's JSON, which exists in
|
|
21316
|
+
-- every version of the function; \`v_ln.variant_id\` only exists where the
|
|
21317
|
+
-- parser was widened by hand.
|
|
21318
|
+
-- ============================================================================
|
|
21319
|
+
|
|
21320
|
+
ALTER TABLE public.plg_inventory_stock_movements
|
|
21321
|
+
ADD COLUMN IF NOT EXISTS variant_id uuid REFERENCES public.product_variants(id) ON DELETE SET NULL;
|
|
21322
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_stock_movements_variant_idx
|
|
21323
|
+
ON public.plg_inventory_stock_movements (tenant_id, variant_id) WHERE variant_id IS NOT NULL;
|
|
21324
|
+
|
|
21325
|
+
DO $patch$
|
|
21326
|
+
DECLARE
|
|
21327
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21328
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21329
|
+
v_cols constant text := 'source_item_type, source_item_id)';
|
|
21330
|
+
v_vals constant text := 'nullif(v_line->>''source_item_id'', '''')::uuid)';
|
|
21331
|
+
BEGIN
|
|
21332
|
+
IF position('source_item_type, source_item_id, variant_id)' in v_def) > 0 THEN
|
|
21333
|
+
RETURN; -- already writes it (a replay, or a later rewrite)
|
|
21334
|
+
END IF;
|
|
21335
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21336
|
+
RAISE EXCEPTION '028: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21337
|
+
END IF;
|
|
21338
|
+
v_def := overlay(v_def PLACING 'source_item_type, source_item_id, variant_id)'
|
|
21339
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21340
|
+
v_def := overlay(v_def PLACING 'nullif(v_line->>''source_item_id'', '''')::uuid, nullif(v_line->>''variant_id'', '''')::uuid)'
|
|
21341
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21342
|
+
EXECUTE v_def;
|
|
21343
|
+
END $patch$;
|
|
21344
|
+
`;
|
|
21345
|
+
var MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = `-- ============================================================================
|
|
21346
|
+
-- 029 \u2014 a reversal undoes the same variant
|
|
21347
|
+
--
|
|
21348
|
+
-- 028 made a receipt keep the variant it was for. \`inventory_reverse\` copies
|
|
21349
|
+
-- the row it undoes and left the variant out, so undoing "6 of M" tried to
|
|
21350
|
+
-- take 6 from the product's default variant \u2014 which has none, and the
|
|
21351
|
+
-- reversal was refused ("would take variant \u2026 below zero"). A receipt by size
|
|
21352
|
+
-- could be made and never corrected.
|
|
21353
|
+
--
|
|
21354
|
+
-- The reversal now carries the variant of the row it reverses.
|
|
21355
|
+
-- Patched in place, like 026\u2013028.
|
|
21356
|
+
-- ============================================================================
|
|
21357
|
+
|
|
21358
|
+
DO $patch$
|
|
21359
|
+
DECLARE
|
|
21360
|
+
v_fn regprocedure := 'public.inventory_reverse(text, text)'::regprocedure;
|
|
21361
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21362
|
+
v_new text;
|
|
21363
|
+
-- The function has two INSERTs (one per branch); both copy the row they undo.
|
|
21364
|
+
v_cols constant text := 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata)';
|
|
21365
|
+
v_vals1 constant text := 'jsonb_build_object(''reversed_movement_id'', r.id))';
|
|
21366
|
+
v_vals2 constant text := '''reversed_kind'', r.kind))';
|
|
21367
|
+
BEGIN
|
|
21368
|
+
IF position('r.variant_id' in v_def) > 0 THEN
|
|
21369
|
+
RETURN; -- already carries it (a replay, or a later rewrite)
|
|
21370
|
+
END IF;
|
|
21371
|
+
v_new := replace(v_def, v_cols, 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata, variant_id)');
|
|
21372
|
+
v_new := replace(v_new, v_vals1, 'jsonb_build_object(''reversed_movement_id'', r.id), r.variant_id)');
|
|
21373
|
+
v_new := replace(v_new, v_vals2, '''reversed_kind'', r.kind), r.variant_id)');
|
|
21374
|
+
-- Every INSERT that gained the column must have gained the value.
|
|
21375
|
+
IF (length(v_new) - length(replace(v_new, 'metadata, variant_id)', ''))) / length('metadata, variant_id)')
|
|
21376
|
+
<> (length(v_new) - length(replace(v_new, ', r.variant_id)', ''))) / length(', r.variant_id)')
|
|
21377
|
+
OR position('metadata, variant_id)' in v_new) = 0 THEN
|
|
21378
|
+
RAISE EXCEPTION '029: inventory_reverse no longer has the INSERTs this extends; patch it by hand';
|
|
21379
|
+
END IF;
|
|
21380
|
+
EXECUTE v_new;
|
|
21381
|
+
END $patch$;
|
|
21382
|
+
`;
|
|
20877
21383
|
var MIGRATIONS = [
|
|
20878
21384
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
20879
21385
|
{ id: "001_the_usage_that_left_stock_can_be_audited", sql: MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED },
|
|
@@ -20900,7 +21406,11 @@ var MIGRATIONS = [
|
|
|
20900
21406
|
{ id: "022_receber_estoque_volta_a_funcionar", sql: MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR },
|
|
20901
21407
|
{ id: "023_a_receita_se_escreve_com_permissao", sql: MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO },
|
|
20902
21408
|
{ id: "024_a_categoria_do_produto_e_do_catalogo", sql: MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO },
|
|
20903
|
-
{ id: "025_a_ficha_tecnica_por_estacao", sql: MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO }
|
|
21409
|
+
{ id: "025_a_ficha_tecnica_por_estacao", sql: MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO },
|
|
21410
|
+
{ id: "026_stock_cost_knows_its_currency", sql: MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY },
|
|
21411
|
+
{ id: "027_a_receipt_names_the_line_it_came_from", sql: MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM },
|
|
21412
|
+
{ id: "028_a_receipt_keeps_the_variant", sql: MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT },
|
|
21413
|
+
{ id: "029_a_reversal_undoes_the_same_variant", sql: MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT }
|
|
20904
21414
|
];
|
|
20905
21415
|
|
|
20906
21416
|
// src/index.ts
|
|
@@ -20940,6 +21450,7 @@ function resolveConfig(options) {
|
|
|
20940
21450
|
locations: options?.locations ?? [],
|
|
20941
21451
|
moduleNav: options?.moduleNav,
|
|
20942
21452
|
extraViews: options?.extraViews ?? [],
|
|
21453
|
+
productDetailTabs: options?.productDetailTabs ?? [],
|
|
20943
21454
|
documentModal: options?.documentModal
|
|
20944
21455
|
};
|
|
20945
21456
|
}
|
|
@@ -20955,7 +21466,7 @@ function createInventoryPlugin(options) {
|
|
|
20955
21466
|
const provider = options?.dataProvider ?? createInventoryDataProvider();
|
|
20956
21467
|
const store = createInventoryStore(provider);
|
|
20957
21468
|
const dashboardWidgets = createInventoryDashboardWidgets({ config, provider, store });
|
|
20958
|
-
const PageComponent = () =>
|
|
21469
|
+
const PageComponent = () => React6.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
|
|
20959
21470
|
return {
|
|
20960
21471
|
id: "inventory",
|
|
20961
21472
|
// Como esta conta refaz o módulo com as opções DELA. Ver PluginRebuild:
|
|
@@ -21124,9 +21635,9 @@ function createInventoryPlugin(options) {
|
|
|
21124
21635
|
// Categories, Units and Suppliers all existed in code and appeared
|
|
21125
21636
|
// nowhere. financial and crm already render through it.
|
|
21126
21637
|
component: (() => {
|
|
21127
|
-
const Tab = () =>
|
|
21638
|
+
const Tab = () => React6.createElement(PluginSettingsPanel, {
|
|
21128
21639
|
title: config.labels.pageTitle,
|
|
21129
|
-
generalSettings:
|
|
21640
|
+
generalSettings: React6.createElement(InventoryGeneralSettings, { config }),
|
|
21130
21641
|
registries: inventoryRegistries,
|
|
21131
21642
|
hostPluginId: "inventory",
|
|
21132
21643
|
routeBase: "/settings/inventory"
|