@fayz-ai/plugin-inventory 0.15.5 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/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/registries.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 +760 -233
- 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 +3 -1
- package/dist/views/ProductCrudForm.d.ts.map +1 -1
- package/dist/views/ProductDetailView.d.ts +20 -0
- 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,9 +1,9 @@
|
|
|
1
|
-
import
|
|
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,
|
|
3
|
-
import { createPluginContext, UNIT_DETAIL_ZONES, PluginSettingsPanel, dedup, RelatedListTab,
|
|
1
|
+
import React14, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
|
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, ModalHeader, ModalTitle, ModalBody, Checkbox, ModalFooter, AiActionButton, ModalDescription, Tooltip, TooltipTrigger, TooltipContent, CardContent, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, StockEntryForm, Skeleton, CurrencyInput, openPrintWindow, printDocument, createXlsxReport, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator, KpiCard, CardHeader, CardTitle } from '@fayz-ai/ui';
|
|
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
|
-
import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, Boxes, Layers, CircleDashed, Search, ListFilter, CalendarRange, PackageCheck, ClipboardCheck, AlertTriangle, Plus,
|
|
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, Sparkles, X, Upload, Building2, Utensils, Clock, Printer, Pencil, Star, Undo2, ArrowUp, ArrowDown, MoreVertical, PackageX } from 'lucide-react';
|
|
7
7
|
import { createStore as createStore$1 } from 'zustand/vanilla';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -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,144 +1753,88 @@ function ProductListView({ onNew, onOpen }) {
|
|
|
1633
1753
|
)
|
|
1634
1754
|
] });
|
|
1635
1755
|
}
|
|
1636
|
-
|
|
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 = React14.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 }) {
|
|
1637
1783
|
const t2 = useTranslation();
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
try {
|
|
1650
|
-
await provider.ensureReferenceData?.();
|
|
1651
|
-
invalidateRelationOptions();
|
|
1652
|
-
} catch (error) {
|
|
1653
|
-
console.warn("[inventory] Could not prepare reference data", error);
|
|
1654
|
-
} finally {
|
|
1655
|
-
if (!cancelled) setReferenceDataReady(true);
|
|
1656
|
-
}
|
|
1657
|
-
})();
|
|
1658
|
-
return () => {
|
|
1659
|
-
cancelled = true;
|
|
1660
|
-
};
|
|
1661
|
-
}, [provider]);
|
|
1662
|
-
React4.useEffect(() => {
|
|
1663
|
-
if (!editId) return;
|
|
1664
|
-
let cancelled = false;
|
|
1665
|
-
(async () => {
|
|
1666
|
-
const p = await provider.getProductById(editId);
|
|
1667
|
-
if (cancelled) return;
|
|
1668
|
-
setHeaderName(p?.name ?? "");
|
|
1669
|
-
setInitialData(p ? flattenProduct(p) : {});
|
|
1670
|
-
})();
|
|
1671
|
-
return () => {
|
|
1672
|
-
cancelled = true;
|
|
1673
|
-
};
|
|
1674
|
-
}, [editId]);
|
|
1675
|
-
const entity = React4.useMemo(
|
|
1676
|
-
() => buildProductEntity(t2, productTypes, currency, void 0, provider),
|
|
1677
|
-
[t2, productTypes, currency, provider]
|
|
1678
|
-
);
|
|
1679
|
-
async function save(values) {
|
|
1680
|
-
const name = String(values.name ?? "").trim();
|
|
1681
|
-
if (!name) {
|
|
1682
|
-
toast.error(t2("common.formIncomplete"));
|
|
1683
|
-
throw new Error("name required");
|
|
1684
|
-
}
|
|
1685
|
-
const classification = {
|
|
1686
|
-
isIngredient: values.isIngredient === true,
|
|
1687
|
-
isForSale: values.isForSale === true,
|
|
1688
|
-
isIntermediate: values.isIntermediate === true,
|
|
1689
|
-
isAsset: values.isAsset === true,
|
|
1690
|
-
isProduced: values.isProduced === true,
|
|
1691
|
-
isResale: values.isResale === true,
|
|
1692
|
-
trackByUnitLabel: values.trackByUnitLabel === true
|
|
1693
|
-
};
|
|
1694
|
-
if (!Object.values(classification).some(Boolean)) {
|
|
1695
|
-
toast.error(t2("inventory.productForm.classificationRequired"));
|
|
1696
|
-
throw new Error("classification required");
|
|
1697
|
-
}
|
|
1698
|
-
const productType = values.productType ?? productTypes[0]?.value;
|
|
1699
|
-
const payload = {
|
|
1700
|
-
classification,
|
|
1701
|
-
extras: {
|
|
1702
|
-
commissionPercent: Number(values.commissionPercent) || 0,
|
|
1703
|
-
chartAccountId: values.chartAccountId ?? "",
|
|
1704
|
-
contentPerUnit: values.contentPerUnit === "" ? void 0 : Number(values.contentPerUnit) || void 0,
|
|
1705
|
-
contentUnitId: values.contentUnitId ?? "",
|
|
1706
|
-
avgPurchasePrice: Number(values.avgPurchasePrice) || void 0,
|
|
1707
|
-
professionalResalePrice: Number(values.professionalResalePrice) || void 0,
|
|
1708
|
-
requireBatchForStock: values.requireBatchForStock === true,
|
|
1709
|
-
requireExpiryForStock: values.requireExpiryForStock === true
|
|
1710
|
-
},
|
|
1711
|
-
...toFichas(values),
|
|
1712
|
-
purchaseUnitId: values.purchaseUnitId ?? "",
|
|
1713
|
-
conversionFactor: Number(values.conversionFactor) || 1,
|
|
1714
|
-
name,
|
|
1715
|
-
sku: values.sku || void 0,
|
|
1716
|
-
barcode: values.barcode || void 0,
|
|
1717
|
-
brand: values.brand || void 0,
|
|
1718
|
-
productType,
|
|
1719
|
-
costPrice: Number(values.costPrice) || 0,
|
|
1720
|
-
// Switching a menu item back to an input must clear the old commercial
|
|
1721
|
-
// price as well as hide it; otherwise reports would keep treating the
|
|
1722
|
-
// ingredient as sellable even though the form no longer showed why.
|
|
1723
|
-
salePrice: classification.isForSale ? Number(values.salePrice) || void 0 : 0,
|
|
1724
|
-
minQuantity: Number(values.minQuantity) || 0,
|
|
1725
|
-
maxQuantity: Number(values.maxQuantity) || void 0,
|
|
1726
|
-
description: values.description || void 0,
|
|
1727
|
-
// Always sent, even when empty: an empty string clears the link, and
|
|
1728
|
-
// `undefined` would leave the previous value in place on edit.
|
|
1729
|
-
categoryId: values.categoryId ?? "",
|
|
1730
|
-
measurementUnitId: values.measurementUnitId ?? "",
|
|
1731
|
-
supplierId: values.supplierId ?? "",
|
|
1732
|
-
defaultLocationId: values.defaultLocationId ?? ""
|
|
1733
|
-
};
|
|
1734
|
-
const imageFile = typeof File !== "undefined" && values._imageFile instanceof File ? values._imageFile : void 0;
|
|
1735
|
-
if (isEdit && editId) {
|
|
1736
|
-
await provider.updateProduct(editId, payload);
|
|
1737
|
-
if (imageFile && provider.uploadProductImage) await provider.uploadProductImage(editId, imageFile);
|
|
1738
|
-
} else {
|
|
1739
|
-
if (await guardProducts() === "blocked") return;
|
|
1740
|
-
const created = await createProduct(payload);
|
|
1741
|
-
if (imageFile && provider.uploadProductImage) {
|
|
1742
|
-
try {
|
|
1743
|
-
await provider.uploadProductImage(created.id, imageFile);
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
toast.error("Produto criado, mas a foto n\xE3o foi enviada.", {
|
|
1746
|
-
description: errorMessage(error)
|
|
1747
|
-
});
|
|
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)
|
|
1748
1795
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
}
|
|
1770
|
-
)
|
|
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
|
+
] })
|
|
1771
1826
|
] });
|
|
1772
1827
|
}
|
|
1773
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
|
+
}
|
|
1837
|
+
|
|
1774
1838
|
// src/lib/recipe-units.ts
|
|
1775
1839
|
var FAMILY_BY_ABBR = {
|
|
1776
1840
|
kg: { family: "mass", toBase: 1e3 },
|
|
@@ -2934,13 +2998,14 @@ var SHEET_LAYOUT = `
|
|
|
2934
2998
|
.rs-comp{grid-template-columns:minmax(0,1fr) minmax(0,1fr) 9rem auto}
|
|
2935
2999
|
.rs-comp>*:not(.rs-comp-menu){grid-column:auto}
|
|
2936
3000
|
.rs-comp>.rs-comp-menu{grid-column:4}
|
|
3001
|
+
.rs-line,.rs-line-head{display:grid;grid-template-columns:minmax(0,2fr) 170px minmax(0,1.2fr) 96px 36px;align-items:center;gap:.5rem;border:0;padding:0}
|
|
3002
|
+
.rs-line>*{grid-column:auto !important}
|
|
3003
|
+
.rs-line>:nth-child(4){justify-content:flex-end}
|
|
2937
3004
|
}
|
|
2938
3005
|
@container (min-width: 60rem){
|
|
2939
3006
|
.rs-head{grid-template-columns:minmax(0,2.2fr) repeat(4,minmax(0,1fr))}
|
|
2940
3007
|
.rs-head>.rs-name{grid-column:auto}
|
|
2941
|
-
.rs-line,.rs-line-head{
|
|
2942
|
-
.rs-line>*{grid-column:auto !important}
|
|
2943
|
-
.rs-line>:nth-child(4){justify-content:flex-end}
|
|
3008
|
+
.rs-line,.rs-line-head{grid-template-columns:minmax(0,2fr) 200px minmax(0,1.4fr) 120px 36px;gap:.75rem}
|
|
2944
3009
|
.rs-out,.rs-out-head{display:grid;grid-template-columns:minmax(0,2fr) 150px 110px 160px 100px 36px;align-items:center;gap:.75rem;border:0;padding:0}
|
|
2945
3010
|
}
|
|
2946
3011
|
`;
|
|
@@ -3354,9 +3419,22 @@ function DetailSkeleton() {
|
|
|
3354
3419
|
/* @__PURE__ */ jsx("div", { className: "rounded-card border p-4 space-y-2", children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ jsx("div", { className: "h-3.5 rounded bg-muted/30 animate-pulse" }, i)) })
|
|
3355
3420
|
] });
|
|
3356
3421
|
}
|
|
3422
|
+
function productRecordTabs({ entity, flat, hasMenu, hasRecipes, withComplements, initialTab, contributedTabs, t: t2, reload }) {
|
|
3423
|
+
const extra = [];
|
|
3424
|
+
if (hasMenu && flat?.isForSale && withComplements) {
|
|
3425
|
+
extra.push({ id: "complements", label: t2("inventory.complements.tab"), icon: "ListPlus", component: ProductComplementsTab });
|
|
3426
|
+
}
|
|
3427
|
+
if (hasRecipes && (flat?.isProduced || initialTab === "recipe")) {
|
|
3428
|
+
extra.push({ id: "recipe", label: t2("inventory.productRecipe.tab"), icon: "ChefHat", component: ProductRecipeTab, props: { onChanged: reload } });
|
|
3429
|
+
}
|
|
3430
|
+
const tabs = entity.detailTabs ?? [];
|
|
3431
|
+
const stockAt = tabs.findIndex((tab) => tab.id === "stock");
|
|
3432
|
+
const own = stockAt < 0 ? [...tabs, ...extra] : [...tabs.slice(0, stockAt), ...extra, ...tabs.slice(stockAt)];
|
|
3433
|
+
return [...own, ...contributedTabs];
|
|
3434
|
+
}
|
|
3357
3435
|
function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
3358
3436
|
const t2 = useTranslation();
|
|
3359
|
-
const { currency, productTypes } = useInventoryConfig();
|
|
3437
|
+
const { currency, productTypes, productDetailTabs: contributedTabs } = useInventoryConfig();
|
|
3360
3438
|
const provider = useInventoryProvider();
|
|
3361
3439
|
const [product, setProduct] = useState(null);
|
|
3362
3440
|
const [loading, setLoading] = useState(true);
|
|
@@ -3374,9 +3452,11 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3374
3452
|
active = false;
|
|
3375
3453
|
};
|
|
3376
3454
|
}, [productId, provider, reloadKey]);
|
|
3455
|
+
const hasMenu = useHasMenu();
|
|
3456
|
+
const hasRecipes = useHasRecipes();
|
|
3377
3457
|
const entity = useMemo(
|
|
3378
|
-
() => buildProductEntity(t2, productTypes, currency, reload),
|
|
3379
|
-
[t2, productTypes, currency, reload]
|
|
3458
|
+
() => buildProductEntity(t2, productTypes, currency, reload, void 0, true, hasMenu, hasRecipes),
|
|
3459
|
+
[t2, productTypes, currency, reload, hasMenu, hasRecipes]
|
|
3380
3460
|
);
|
|
3381
3461
|
const flat = useMemo(
|
|
3382
3462
|
() => product ? flattenProduct(product) : null,
|
|
@@ -3388,16 +3468,7 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3388
3468
|
}, [provider, productId, reload, flat]);
|
|
3389
3469
|
const [photoOpen, setPhotoOpen] = useState(false);
|
|
3390
3470
|
const entityForDetail = useMemo(() => {
|
|
3391
|
-
const
|
|
3392
|
-
if (flat?.isForSale && provider.getProductOptionGroups) {
|
|
3393
|
-
extra.push({ id: "complements", label: t2("inventory.complements.tab"), icon: "ListPlus", component: ProductComplementsTab });
|
|
3394
|
-
}
|
|
3395
|
-
if (flat?.isProduced || initialTab === "recipe") {
|
|
3396
|
-
extra.push({ id: "recipe", label: t2("inventory.productRecipe.tab"), icon: "ChefHat", component: ProductRecipeTab, props: { onChanged: reload } });
|
|
3397
|
-
}
|
|
3398
|
-
const tabs = entity.detailTabs ?? [];
|
|
3399
|
-
const stockAt = tabs.findIndex((tab) => tab.id === "stock");
|
|
3400
|
-
const detailTabs = stockAt < 0 ? [...tabs, ...extra] : [...tabs.slice(0, stockAt), ...extra, ...tabs.slice(stockAt)];
|
|
3471
|
+
const detailTabs = productRecordTabs({ entity, flat, hasMenu, hasRecipes, withComplements: Boolean(provider.getProductOptionGroups), initialTab, contributedTabs, t: t2, reload });
|
|
3401
3472
|
return {
|
|
3402
3473
|
...entity,
|
|
3403
3474
|
detailTabs,
|
|
@@ -3408,7 +3479,7 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3408
3479
|
} }] : []
|
|
3409
3480
|
]
|
|
3410
3481
|
};
|
|
3411
|
-
}, [entity, flat?.isForSale, flat?.isProduced, initialTab, provider, reload, t2]);
|
|
3482
|
+
}, [entity, hasMenu, hasRecipes, flat?.isForSale, flat?.isProduced, initialTab, provider, reload, t2, contributedTabs]);
|
|
3412
3483
|
if (loading) {
|
|
3413
3484
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
3414
3485
|
/* @__PURE__ */ jsx(SubpageHeader, { title: t2("inventory.nav.products"), onBack }),
|
|
@@ -3443,6 +3514,154 @@ function ProductDetailView({ productId, initialTab, onBack, onEdit }) {
|
|
|
3443
3514
|
photoOpen && /* @__PURE__ */ jsx(GenerateImageDialog, { product, onClose: () => setPhotoOpen(false), onApplied: reload })
|
|
3444
3515
|
] });
|
|
3445
3516
|
}
|
|
3517
|
+
function ProductCrudForm({ editId, onSaved, onOpenTab }) {
|
|
3518
|
+
const t2 = useTranslation();
|
|
3519
|
+
const { productTypes, currency, productDetailTabs: contributedTabs } = useInventoryConfig();
|
|
3520
|
+
const provider = useInventoryProvider();
|
|
3521
|
+
const createProduct = useInventoryStore((s) => s.createProduct);
|
|
3522
|
+
const guardProducts = useLimitGuard("products");
|
|
3523
|
+
const isEdit = !!editId;
|
|
3524
|
+
const [initialData, setInitialData] = React14.useState(isEdit ? null : {});
|
|
3525
|
+
const [headerName, setHeaderName] = React14.useState("");
|
|
3526
|
+
const [referenceDataReady, setReferenceDataReady] = React14.useState(false);
|
|
3527
|
+
React14.useEffect(() => {
|
|
3528
|
+
let cancelled = false;
|
|
3529
|
+
(async () => {
|
|
3530
|
+
try {
|
|
3531
|
+
await provider.ensureReferenceData?.();
|
|
3532
|
+
invalidateRelationOptions();
|
|
3533
|
+
} catch (error) {
|
|
3534
|
+
console.warn("[inventory] Could not prepare reference data", error);
|
|
3535
|
+
} finally {
|
|
3536
|
+
if (!cancelled) setReferenceDataReady(true);
|
|
3537
|
+
}
|
|
3538
|
+
})();
|
|
3539
|
+
return () => {
|
|
3540
|
+
cancelled = true;
|
|
3541
|
+
};
|
|
3542
|
+
}, [provider]);
|
|
3543
|
+
React14.useEffect(() => {
|
|
3544
|
+
if (!editId) return;
|
|
3545
|
+
let cancelled = false;
|
|
3546
|
+
(async () => {
|
|
3547
|
+
const p = await provider.getProductById(editId);
|
|
3548
|
+
if (cancelled) return;
|
|
3549
|
+
setHeaderName(p?.name ?? "");
|
|
3550
|
+
setInitialData(p ? flattenProduct(p) : {});
|
|
3551
|
+
})();
|
|
3552
|
+
return () => {
|
|
3553
|
+
cancelled = true;
|
|
3554
|
+
};
|
|
3555
|
+
}, [editId]);
|
|
3556
|
+
const hasMenu = useHasMenu();
|
|
3557
|
+
const hasRecipes = useHasRecipes();
|
|
3558
|
+
const entity = React14.useMemo(
|
|
3559
|
+
() => buildProductEntity(t2, productTypes, currency, void 0, provider, true, hasMenu, hasRecipes),
|
|
3560
|
+
[t2, productTypes, currency, provider, hasMenu, hasRecipes]
|
|
3561
|
+
);
|
|
3562
|
+
async function save(values) {
|
|
3563
|
+
const name = String(values.name ?? "").trim();
|
|
3564
|
+
if (!name) {
|
|
3565
|
+
toast.error(t2("common.formIncomplete"));
|
|
3566
|
+
throw new Error("name required");
|
|
3567
|
+
}
|
|
3568
|
+
const classification = {
|
|
3569
|
+
isIngredient: values.isIngredient === true,
|
|
3570
|
+
isForSale: values.isForSale === true,
|
|
3571
|
+
isIntermediate: values.isIntermediate === true,
|
|
3572
|
+
isAsset: values.isAsset === true,
|
|
3573
|
+
isProduced: values.isProduced === true,
|
|
3574
|
+
isResale: values.isResale === true,
|
|
3575
|
+
trackByUnitLabel: values.trackByUnitLabel === true
|
|
3576
|
+
};
|
|
3577
|
+
if (!Object.values(classification).some(Boolean)) {
|
|
3578
|
+
toast.error(t2("inventory.productForm.classificationRequired"));
|
|
3579
|
+
throw new Error("classification required");
|
|
3580
|
+
}
|
|
3581
|
+
const productType = values.productType ?? productTypes[0]?.value;
|
|
3582
|
+
const payload = {
|
|
3583
|
+
classification,
|
|
3584
|
+
extras: {
|
|
3585
|
+
commissionPercent: Number(values.commissionPercent) || 0,
|
|
3586
|
+
chartAccountId: values.chartAccountId ?? "",
|
|
3587
|
+
contentPerUnit: values.contentPerUnit === "" ? void 0 : Number(values.contentPerUnit) || void 0,
|
|
3588
|
+
contentUnitId: values.contentUnitId ?? "",
|
|
3589
|
+
avgPurchasePrice: Number(values.avgPurchasePrice) || void 0,
|
|
3590
|
+
professionalResalePrice: Number(values.professionalResalePrice) || void 0,
|
|
3591
|
+
requireBatchForStock: values.requireBatchForStock === true,
|
|
3592
|
+
requireExpiryForStock: values.requireExpiryForStock === true
|
|
3593
|
+
},
|
|
3594
|
+
...toFichas(values),
|
|
3595
|
+
purchaseUnitId: values.purchaseUnitId ?? "",
|
|
3596
|
+
conversionFactor: Number(values.conversionFactor) || 1,
|
|
3597
|
+
name,
|
|
3598
|
+
sku: values.sku || void 0,
|
|
3599
|
+
barcode: values.barcode || void 0,
|
|
3600
|
+
brand: values.brand || void 0,
|
|
3601
|
+
productType,
|
|
3602
|
+
costPrice: Number(values.costPrice) || 0,
|
|
3603
|
+
// Switching a menu item back to an input must clear the old commercial
|
|
3604
|
+
// price as well as hide it; otherwise reports would keep treating the
|
|
3605
|
+
// ingredient as sellable even though the form no longer showed why.
|
|
3606
|
+
salePrice: classification.isForSale ? Number(values.salePrice) || void 0 : 0,
|
|
3607
|
+
minQuantity: Number(values.minQuantity) || 0,
|
|
3608
|
+
maxQuantity: Number(values.maxQuantity) || void 0,
|
|
3609
|
+
description: values.description || void 0,
|
|
3610
|
+
// Always sent, even when empty: an empty string clears the link, and
|
|
3611
|
+
// `undefined` would leave the previous value in place on edit.
|
|
3612
|
+
categoryId: values.categoryId ?? "",
|
|
3613
|
+
measurementUnitId: values.measurementUnitId ?? "",
|
|
3614
|
+
supplierId: values.supplierId ?? "",
|
|
3615
|
+
defaultLocationId: values.defaultLocationId ?? ""
|
|
3616
|
+
};
|
|
3617
|
+
const imageFile = typeof File !== "undefined" && values._imageFile instanceof File ? values._imageFile : void 0;
|
|
3618
|
+
if (isEdit && editId) {
|
|
3619
|
+
await provider.updateProduct(editId, payload);
|
|
3620
|
+
if (imageFile && provider.uploadProductImage) await provider.uploadProductImage(editId, imageFile);
|
|
3621
|
+
} else {
|
|
3622
|
+
if (await guardProducts() === "blocked") return;
|
|
3623
|
+
const created = await createProduct(payload);
|
|
3624
|
+
if (imageFile && provider.uploadProductImage) {
|
|
3625
|
+
try {
|
|
3626
|
+
await provider.uploadProductImage(created.id, imageFile);
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
toast.error("Produto criado, mas a foto n\xE3o foi enviada.", {
|
|
3629
|
+
description: errorMessage(error)
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
invalidateLimit("products");
|
|
3634
|
+
}
|
|
3635
|
+
onSaved?.();
|
|
3636
|
+
}
|
|
3637
|
+
const formEntity = React14.useMemo(() => {
|
|
3638
|
+
if (!isEdit || !initialData) return entity;
|
|
3639
|
+
return {
|
|
3640
|
+
...entity,
|
|
3641
|
+
detailTabs: productRecordTabs({ entity, flat: initialData, hasMenu, hasRecipes, withComplements: Boolean(provider.getProductOptionGroups), contributedTabs, t: t2 })
|
|
3642
|
+
};
|
|
3643
|
+
}, [entity, isEdit, initialData, hasMenu, hasRecipes, provider, contributedTabs, t2]);
|
|
3644
|
+
const ready = Boolean(initialData) && referenceDataReady;
|
|
3645
|
+
const title = isEdit ? headerName || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
|
|
3646
|
+
const subtitle = isEdit ? void 0 : t2("inventory.productForm.addToCatalog");
|
|
3647
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
3648
|
+
(!isEdit || !ready) && /* @__PURE__ */ jsx(SubpageHeader, { title, subtitle, onBack: onSaved, parentLabel: t2("inventory.nav.products") }),
|
|
3649
|
+
!ready && /* @__PURE__ */ jsx(CrudFormSkeleton, { twoColumn: true, tabs: isEdit }),
|
|
3650
|
+
ready && /* @__PURE__ */ jsx(
|
|
3651
|
+
CrudFormPage,
|
|
3652
|
+
{
|
|
3653
|
+
entityDef: formEntity,
|
|
3654
|
+
mode: isEdit ? "edit" : "create",
|
|
3655
|
+
initialData,
|
|
3656
|
+
onSubmit: save,
|
|
3657
|
+
onCancel: () => onSaved?.(),
|
|
3658
|
+
namePlural: t2("inventory.nav.products"),
|
|
3659
|
+
hideBreadcrumb: !isEdit,
|
|
3660
|
+
onRecordTab: isEdit ? onOpenTab : void 0
|
|
3661
|
+
}
|
|
3662
|
+
)
|
|
3663
|
+
] });
|
|
3664
|
+
}
|
|
3446
3665
|
function signedQuantity(q) {
|
|
3447
3666
|
const n = Number(q) || 0;
|
|
3448
3667
|
return n > 0 ? `+${n}` : String(n);
|
|
@@ -5632,7 +5851,7 @@ function parseQuantity(raw) {
|
|
|
5632
5851
|
if (!Number.isFinite(parsed) || parsed < 0) return void 0;
|
|
5633
5852
|
return parsed;
|
|
5634
5853
|
}
|
|
5635
|
-
var CountedField =
|
|
5854
|
+
var CountedField = React14.memo(function CountedField2({
|
|
5636
5855
|
entry,
|
|
5637
5856
|
value,
|
|
5638
5857
|
onChange,
|
|
@@ -6217,47 +6436,6 @@ function BulkProductEditView() {
|
|
|
6217
6436
|
}
|
|
6218
6437
|
);
|
|
6219
6438
|
}
|
|
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
6439
|
var STEPS = [
|
|
6262
6440
|
{ id: "welcome", icon: Package, titleKey: "inventory.onboarding.welcome", descKey: "inventory.onboarding.description" },
|
|
6263
6441
|
{ id: "units", icon: Ruler, titleKey: "inventory.onboarding.units.title", descKey: "inventory.onboarding.units.description" },
|
|
@@ -6307,7 +6485,7 @@ function InventoryOnboarding({ onComplete }) {
|
|
|
6307
6485
|
] })
|
|
6308
6486
|
] }) });
|
|
6309
6487
|
}
|
|
6310
|
-
function buildNav(config, view, navigate, t2) {
|
|
6488
|
+
function buildNav(config, view, navigate, t2, house) {
|
|
6311
6489
|
const items = [
|
|
6312
6490
|
// Tudo o que se faz COM o catálogo mora aqui: a lista, como ela se agrupa
|
|
6313
6491
|
// (Categorias) e como se mexe em muitos de uma vez. A primeira filha
|
|
@@ -6325,7 +6503,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6325
6503
|
active: view.startsWith("products") || view === "product-categories" || view === "bulk-products",
|
|
6326
6504
|
children: [
|
|
6327
6505
|
{ 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-
|
|
6506
|
+
{ 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
6507
|
{ id: "product-categories", label: t2("registry.product-categories"), active: view === "product-categories", onClick: () => navigate("product-categories") },
|
|
6330
6508
|
{ id: "bulk-products", label: "Altera\xE7\xE3o em massa", active: view === "bulk-products", onClick: () => navigate("bulk-products") }
|
|
6331
6509
|
]
|
|
@@ -6374,7 +6552,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6374
6552
|
// panela rende.
|
|
6375
6553
|
{ id: "transfers", label: "Transfer\xEAncias", icon: "ArrowLeftRight", active: view === "transfers", onClick: () => navigate("transfers") },
|
|
6376
6554
|
{ 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") },
|
|
6555
|
+
...house.requisitions ? [{ id: "requisitions", label: "Requisi\xE7\xF5es", icon: "ClipboardList", active: view === "requisitions", onClick: () => navigate("requisitions") }] : [],
|
|
6378
6556
|
// Fichas Técnicas sits here, after Requisições, as in V1 (pushed below
|
|
6379
6557
|
// only when the house uses recipes).
|
|
6380
6558
|
...config.modules.recipes ? [{
|
|
@@ -6392,13 +6570,13 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6392
6570
|
//
|
|
6393
6571
|
// "Nova contagem" não está na navegação: era uma AÇÃO dentro de um menu, e
|
|
6394
6572
|
// a lista de contagens já tem o "+" no cabeçalho dela.
|
|
6395
|
-
{
|
|
6573
|
+
...house.usageAudit ? [{
|
|
6396
6574
|
id: "stock-usage-audit",
|
|
6397
6575
|
label: t2("inventory.nav.usageAudit"),
|
|
6398
6576
|
icon: "ScanSearch",
|
|
6399
6577
|
active: view === "stock-usage-audit",
|
|
6400
6578
|
onClick: () => navigate("stock-usage-audit")
|
|
6401
|
-
},
|
|
6579
|
+
}] : [],
|
|
6402
6580
|
// Produção fica de fora por decisão do founder: são seis telas e um assunto
|
|
6403
6581
|
// próprio, não um buraco do estoque.
|
|
6404
6582
|
//
|
|
@@ -6413,11 +6591,12 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6413
6591
|
active: ["stock-locations", "temperature"].some((v) => view === v),
|
|
6414
6592
|
children: [
|
|
6415
6593
|
{ 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") }
|
|
6594
|
+
...house.temperature ? [{ id: "temperature", label: "Temperatura", active: view === "temperature", onClick: () => navigate("temperature") }] : []
|
|
6417
6595
|
]
|
|
6418
6596
|
}
|
|
6419
6597
|
];
|
|
6420
6598
|
for (const extra of config.extraViews) {
|
|
6599
|
+
if (house.hiddenViews.includes(extra.id)) continue;
|
|
6421
6600
|
const own = [extra.id, ...(extra.children ?? []).map((c) => c.id)];
|
|
6422
6601
|
const hits = (id) => view === id || view.startsWith(`${id}/`) || view.startsWith(`${id}:`);
|
|
6423
6602
|
items.push({
|
|
@@ -6441,7 +6620,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
6441
6620
|
}
|
|
6442
6621
|
function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
6443
6622
|
const t2 = useTranslation();
|
|
6444
|
-
const houseModules =
|
|
6623
|
+
const houseModules = useInventoryHouseModules();
|
|
6445
6624
|
const recipesOn = houseModules.get("recipes");
|
|
6446
6625
|
const config = useMemo(
|
|
6447
6626
|
() => appConfig.modules.recipes && recipesOn ? appConfig : { ...appConfig, modules: { ...appConfig.modules, recipes: false } },
|
|
@@ -6462,15 +6641,20 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6462
6641
|
"products-recipe": 1,
|
|
6463
6642
|
settings: 1
|
|
6464
6643
|
}, "dashboard");
|
|
6465
|
-
const checkExistingData =
|
|
6644
|
+
const checkExistingData = React14.useCallback(
|
|
6466
6645
|
async () => (await provider.getProducts({ pageSize: 1 })).total > 0,
|
|
6467
6646
|
[provider]
|
|
6468
6647
|
);
|
|
6469
6648
|
const onboarding = usePluginOnboardingGate("saas-core:inventory-onboarded", checkExistingData);
|
|
6470
6649
|
const isSettings = view === "settings";
|
|
6471
6650
|
const isSummary = view === "dashboard" || view === "";
|
|
6472
|
-
const nav = buildNav(config, view, navigate, t2
|
|
6473
|
-
|
|
6651
|
+
const nav = buildNav(config, view, navigate, t2, {
|
|
6652
|
+
requisitions: houseModules.get("requisitions"),
|
|
6653
|
+
usageAudit: houseModules.get("usageAudit"),
|
|
6654
|
+
temperature: houseModules.get("temperature"),
|
|
6655
|
+
hiddenViews: houseModules.get("hiddenViews")
|
|
6656
|
+
});
|
|
6657
|
+
const completeOnboarding = React14.useCallback(async () => {
|
|
6474
6658
|
await provider.ensureReferenceData?.();
|
|
6475
6659
|
onboarding.complete();
|
|
6476
6660
|
}, [provider, onboarding.complete]);
|
|
@@ -6533,11 +6717,16 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6533
6717
|
{ id: "products", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onOpen: (id) => navigate(`products-detail:${id}`) }) },
|
|
6534
6718
|
{ id: "products-list", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onOpen: (id) => navigate(`products-detail:${id}`) }) },
|
|
6535
6719
|
{ id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
|
|
6536
|
-
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate(`products-detail:${id}`) }) },
|
|
6720
|
+
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate(`products-detail:${id}`), onOpenTab: (tab) => navigate(`products-${tab}:${id}`) }) },
|
|
6537
6721
|
{ 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
|
-
|
|
6722
|
+
// Every tab writes `/products/<id>/<tab>` into the URL, so each of those
|
|
6723
|
+
// links has to come back to the same page with that tab open. Only
|
|
6724
|
+
// `stock` had a route: reloading on any other tab fell through to the
|
|
6725
|
+
// module's first screen.
|
|
6726
|
+
...["stock", "variants", "barcodes", "complements", ...config.productDetailTabs.map((tab) => tab.id)].map((tab) => ({
|
|
6727
|
+
id: `products-${tab}`,
|
|
6728
|
+
render: ({ id }) => /* @__PURE__ */ jsx(ProductDetailView, { productId: id, initialTab: tab, onBack: () => navigate("products-list"), onEdit: () => navigate(`products-edit:${id}`) })
|
|
6729
|
+
}))
|
|
6541
6730
|
] : [],
|
|
6542
6731
|
{ id: "stock-entry", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
|
|
6543
6732
|
{ id: "stock-exit", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
|
|
@@ -6577,14 +6766,19 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
|
|
|
6577
6766
|
// topo e largá-lo numa fileira própria, encostado à direita do corpo. Com
|
|
6578
6767
|
// isso, Locais e Categorias eram as duas únicas listas do Estoque cujo botão
|
|
6579
6768
|
// de criar ficava em outro lugar que o de Produtos e o de Contagens.
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6769
|
+
// The module writes a view id as a path (`product-categories` is
|
|
6770
|
+
// `/product/categories`), so the registry's base path and its own
|
|
6771
|
+
// new/edit addresses have to be spelled the same way or "New" lands on 404.
|
|
6772
|
+
...["", "-new", "-edit", "-detail"].flatMap((suffix) => [
|
|
6773
|
+
{ id: `product-categories${suffix}`, render: () => {
|
|
6774
|
+
const reg = registries?.find((r) => r.id === "product-categories");
|
|
6775
|
+
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"] });
|
|
6776
|
+
} },
|
|
6777
|
+
{ id: `stock-locations${suffix}`, render: () => {
|
|
6778
|
+
const reg = registries?.find((r) => r.id === "stock-locations");
|
|
6779
|
+
return reg ? /* @__PURE__ */ jsx(RegistryCrudView, { registry: reg, basePath: "/inventory/stock/locations" }) : /* @__PURE__ */ jsx(StockLocationsView, {});
|
|
6780
|
+
} }
|
|
6781
|
+
]),
|
|
6588
6782
|
{ id: "temperature", render: () => /* @__PURE__ */ jsx(TemperatureView, {}) },
|
|
6589
6783
|
{ id: "bulk-products", render: () => /* @__PURE__ */ jsx(BulkProductEditView, {}) },
|
|
6590
6784
|
{ id: "stock-indicators", render: () => /* @__PURE__ */ jsx(StockIndicatorsView, {}) },
|
|
@@ -8406,6 +8600,42 @@ function createSupabaseInventoryProvider() {
|
|
|
8406
8600
|
});
|
|
8407
8601
|
if (error) throw new Error(error.message);
|
|
8408
8602
|
},
|
|
8603
|
+
async getProductVariants(productId) {
|
|
8604
|
+
const { core } = getClients();
|
|
8605
|
+
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 });
|
|
8606
|
+
const rows = data ?? [];
|
|
8607
|
+
if (rows.length === 0) return [];
|
|
8608
|
+
const balances = await core.from("stock_balances").select("variant_id, on_hand").in("variant_id", rows.map((r) => r.id));
|
|
8609
|
+
const onHand = /* @__PURE__ */ new Map();
|
|
8610
|
+
for (const b of balances.data ?? []) {
|
|
8611
|
+
onHand.set(b.variant_id, (onHand.get(b.variant_id) ?? 0) + (Number(b.on_hand) || 0));
|
|
8612
|
+
}
|
|
8613
|
+
return rows.map((r) => ({
|
|
8614
|
+
id: r.id,
|
|
8615
|
+
name: r.name,
|
|
8616
|
+
sku: r.sku ?? void 0,
|
|
8617
|
+
isDefault: !!r.is_default,
|
|
8618
|
+
isActive: r.is_active !== false,
|
|
8619
|
+
onHand: onHand.get(r.id) ?? 0
|
|
8620
|
+
}));
|
|
8621
|
+
},
|
|
8622
|
+
async addProductVariant(productId, input) {
|
|
8623
|
+
const { core } = getClients();
|
|
8624
|
+
const existing = await provider.getProductVariants(productId);
|
|
8625
|
+
const { error } = await core.from("product_variants").insert({
|
|
8626
|
+
tenant_id: getTenantId(),
|
|
8627
|
+
product_id: productId,
|
|
8628
|
+
name: input.name.trim(),
|
|
8629
|
+
sku: input.sku?.trim() || null,
|
|
8630
|
+
sort_order: existing.length
|
|
8631
|
+
});
|
|
8632
|
+
if (error) throw new Error(error.message);
|
|
8633
|
+
},
|
|
8634
|
+
async setProductVariantActive(id, active) {
|
|
8635
|
+
const { core } = getClients();
|
|
8636
|
+
const { error } = await core.from("product_variants").update({ is_active: active }).eq("id", id);
|
|
8637
|
+
if (error) throw new Error(error.message);
|
|
8638
|
+
},
|
|
8409
8639
|
async removeProductBarcode(id) {
|
|
8410
8640
|
const { core } = getClients();
|
|
8411
8641
|
const { error } = await core.from(T.productBarcodes).delete().eq("id", id);
|
|
@@ -9755,6 +9985,9 @@ var supplierEntity = {
|
|
|
9755
9985
|
// office that invoices, the warehouse the goods leave from). `layout:
|
|
9756
9986
|
// 'person'` gives the record an "Addresses" tab backed by public.addresses,
|
|
9757
9987
|
// which is the same relation clients and patients already use.
|
|
9988
|
+
// Declares the supplier as someone the house pays, which is what keeps
|
|
9989
|
+
// the "Bank accounts" section on the record.
|
|
9990
|
+
{ key: "payeeAccount", label: "Bank account", type: "bank", showInTable: false, showInDetail: false },
|
|
9758
9991
|
{ key: "notes", label: "Notes", type: "textarea" },
|
|
9759
9992
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
9760
9993
|
],
|
|
@@ -10396,6 +10629,11 @@ var en = {
|
|
|
10396
10629
|
"inventory.settings.modulesDesc": "What this house shows inside Inventory",
|
|
10397
10630
|
"inventory.settings.recipes": "Recipes",
|
|
10398
10631
|
"inventory.settings.recipesDesc": "The bill of materials that says what each product consumes",
|
|
10632
|
+
"inventory.settings.requisitions": "Requisitions",
|
|
10633
|
+
"inventory.settings.requisitionsDesc": "Internal requests for material between areas",
|
|
10634
|
+
"inventory.settings.usageAuditDesc": "What left stock against what was sold",
|
|
10635
|
+
"inventory.settings.temperature": "Temperature",
|
|
10636
|
+
"inventory.settings.temperatureDesc": "Temperature log for refrigerated locations",
|
|
10399
10637
|
"inventory.settings.stockManagement": "Stock Management",
|
|
10400
10638
|
"inventory.settings.stockManagementDesc": "How stock levels are tracked",
|
|
10401
10639
|
"inventory.settings.lowStockAlerts": "Low stock alerts",
|
|
@@ -10518,6 +10756,18 @@ var en = {
|
|
|
10518
10756
|
"inventory.productType.sale": "For sale",
|
|
10519
10757
|
"inventory.productType.intermediate": "Intermediate",
|
|
10520
10758
|
"inventory.productType.asset": "Asset",
|
|
10759
|
+
"inventory.variants.tab": "Variants",
|
|
10760
|
+
"inventory.variants.name": "Variant",
|
|
10761
|
+
"inventory.variants.namePlaceholder": "E.g. M, L, XL, Blue",
|
|
10762
|
+
"inventory.variants.sku": "SKU",
|
|
10763
|
+
"inventory.variants.skuPlaceholder": "Optional",
|
|
10764
|
+
"inventory.variants.default": "Default",
|
|
10765
|
+
"inventory.variants.empty": "No variants. The product is sold and stocked as a single item.",
|
|
10766
|
+
"inventory.variants.duplicate": "A variant with this name already exists.",
|
|
10767
|
+
"inventory.variants.loadFailed": "Could not load the variants.",
|
|
10768
|
+
"inventory.variants.saveFailed": "Could not save the variant.",
|
|
10769
|
+
"inventory.variants.unavailable": "Variants are not available in this workspace.",
|
|
10770
|
+
"inventory.variants.onHand": "{{count}} in stock",
|
|
10521
10771
|
"inventory.barcodes.tab": "Barcodes",
|
|
10522
10772
|
"inventory.barcodes.code": "Barcode",
|
|
10523
10773
|
"inventory.barcodes.codePlaceholder": "Type or scan",
|
|
@@ -11233,6 +11483,11 @@ var ptBR = {
|
|
|
11233
11483
|
"inventory.settings.modulesDesc": "O que desta casa aparece no Estoque",
|
|
11234
11484
|
"inventory.settings.recipes": "Receitas",
|
|
11235
11485
|
"inventory.settings.recipesDesc": "A ficha t\xE9cnica que diz quanto cada produto consome",
|
|
11486
|
+
"inventory.settings.requisitions": "Requisi\xE7\xF5es",
|
|
11487
|
+
"inventory.settings.requisitionsDesc": "Pedidos internos de material entre setores",
|
|
11488
|
+
"inventory.settings.usageAuditDesc": "O que saiu do estoque contra o que foi vendido",
|
|
11489
|
+
"inventory.settings.temperature": "Temperatura",
|
|
11490
|
+
"inventory.settings.temperatureDesc": "Registro de temperatura dos locais refrigerados",
|
|
11236
11491
|
"inventory.settings.stockManagement": "Gest\xE3o de Estoque",
|
|
11237
11492
|
"inventory.settings.stockManagementDesc": "Como os n\xEDveis de estoque s\xE3o rastreados",
|
|
11238
11493
|
"inventory.settings.lowStockAlerts": "Alertas de estoque baixo",
|
|
@@ -11355,6 +11610,18 @@ var ptBR = {
|
|
|
11355
11610
|
"inventory.productType.sale": "Venda",
|
|
11356
11611
|
"inventory.productType.intermediate": "Intermedi\xE1rio",
|
|
11357
11612
|
"inventory.productType.asset": "Patrim\xF4nio",
|
|
11613
|
+
"inventory.variants.tab": "Varia\xE7\xF5es",
|
|
11614
|
+
"inventory.variants.name": "Varia\xE7\xE3o",
|
|
11615
|
+
"inventory.variants.namePlaceholder": "Ex.: M, G, GG, Azul",
|
|
11616
|
+
"inventory.variants.sku": "SKU",
|
|
11617
|
+
"inventory.variants.skuPlaceholder": "Opcional",
|
|
11618
|
+
"inventory.variants.default": "Padr\xE3o",
|
|
11619
|
+
"inventory.variants.empty": "Nenhuma varia\xE7\xE3o. O produto \xE9 vendido e estocado como um item s\xF3.",
|
|
11620
|
+
"inventory.variants.duplicate": "J\xE1 existe uma varia\xE7\xE3o com esse nome.",
|
|
11621
|
+
"inventory.variants.loadFailed": "N\xE3o foi poss\xEDvel carregar as varia\xE7\xF5es.",
|
|
11622
|
+
"inventory.variants.saveFailed": "N\xE3o foi poss\xEDvel salvar a varia\xE7\xE3o.",
|
|
11623
|
+
"inventory.variants.unavailable": "Varia\xE7\xF5es n\xE3o est\xE3o dispon\xEDveis nesta conta.",
|
|
11624
|
+
"inventory.variants.onHand": "{{count}} em estoque",
|
|
11358
11625
|
"inventory.barcodes.tab": "C\xF3digos de Barras",
|
|
11359
11626
|
"inventory.barcodes.code": "C\xF3digo de barras",
|
|
11360
11627
|
"inventory.barcodes.codePlaceholder": "Digite ou bipe",
|
|
@@ -20874,6 +21141,261 @@ $$;
|
|
|
20874
21141
|
REVOKE ALL ON FUNCTION public.inventory_save_recipe(jsonb) FROM PUBLIC, anon;
|
|
20875
21142
|
GRANT EXECUTE ON FUNCTION public.inventory_save_recipe(jsonb) TO authenticated;
|
|
20876
21143
|
`;
|
|
21144
|
+
var MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY = `-- ============================================================================
|
|
21145
|
+
-- 026 \u2014 stock cost knows its currency
|
|
21146
|
+
--
|
|
21147
|
+
-- \`unit_cost\` was a bare number. With a shop in Rio and one in Barcelona, and
|
|
21148
|
+
-- shirts paid for in dollars, three currencies would have met in one weighted
|
|
21149
|
+
-- average.
|
|
21150
|
+
--
|
|
21151
|
+
-- The rule: the cost a position carries is ALWAYS in the currency of the unit
|
|
21152
|
+
-- that holds the stock, so every sum and average that already exists stays
|
|
21153
|
+
-- right. What was actually paid, when it was paid in something else, is kept
|
|
21154
|
+
-- beside it (\`source_currency\`, \`source_unit_cost\`, \`fx_rate\`) and converted
|
|
21155
|
+
-- once, on the way in.
|
|
21156
|
+
--
|
|
21157
|
+
-- A receipt says the currency on the document (or the line):
|
|
21158
|
+
-- inventory_receive(loc, lines, '{"currency":"USD","fx_rate":5.42}')
|
|
21159
|
+
-- With no rate given, the tenant's rate for the day is used; with none on
|
|
21160
|
+
-- file, the receipt is refused rather than booked at a made-up cost.
|
|
21161
|
+
--
|
|
21162
|
+
-- Moving stock between units of different currencies is refused for now: the
|
|
21163
|
+
-- same cost number would land in the other currency unconverted.
|
|
21164
|
+
-- ============================================================================
|
|
21165
|
+
|
|
21166
|
+
DO $$
|
|
21167
|
+
BEGIN
|
|
21168
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
21169
|
+
WHERE table_schema = 'public' AND table_name = 'plg_inventory_stock_movements' AND column_name = 'currency') THEN
|
|
21170
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
21171
|
+
ALTER TABLE public.plg_inventory_stock_movements ALTER COLUMN currency DROP DEFAULT;
|
|
21172
|
+
END IF;
|
|
21173
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
21174
|
+
WHERE table_schema = 'public' AND table_name = 'plg_inventory_stock_positions' AND column_name = 'currency') THEN
|
|
21175
|
+
ALTER TABLE public.plg_inventory_stock_positions ADD COLUMN currency text NOT NULL DEFAULT 'BRL';
|
|
21176
|
+
ALTER TABLE public.plg_inventory_stock_positions ALTER COLUMN currency DROP DEFAULT;
|
|
21177
|
+
END IF;
|
|
21178
|
+
END $$;
|
|
21179
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS source_currency text;
|
|
21180
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS source_unit_cost numeric;
|
|
21181
|
+
ALTER TABLE public.plg_inventory_stock_movements ADD COLUMN IF NOT EXISTS fx_rate numeric;
|
|
21182
|
+
|
|
21183
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.currency IS 'Currency of unit_cost/total_cost: the unit that holds the stock.';
|
|
21184
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.source_currency IS 'Currency the goods were paid in, when not the unit''s.';
|
|
21185
|
+
COMMENT ON COLUMN public.plg_inventory_stock_movements.source_unit_cost IS 'Unit cost as paid, in source_currency.';
|
|
21186
|
+
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.';
|
|
21187
|
+
COMMENT ON COLUMN public.plg_inventory_stock_positions.currency IS 'Currency of unit_cost: the unit that holds this balance.';
|
|
21188
|
+
|
|
21189
|
+
CREATE OR REPLACE FUNCTION app.inventory_movements_currency()
|
|
21190
|
+
RETURNS trigger
|
|
21191
|
+
LANGUAGE plpgsql
|
|
21192
|
+
SECURITY DEFINER
|
|
21193
|
+
SET search_path TO ''
|
|
21194
|
+
AS $function$
|
|
21195
|
+
DECLARE
|
|
21196
|
+
v_unit_cur text := app.unit_currency(NEW.tenant_id, NEW.unit_id);
|
|
21197
|
+
v_dst_unit uuid;
|
|
21198
|
+
v_src text := nullif(upper(btrim(coalesce(NEW.source_currency, ''))), '');
|
|
21199
|
+
BEGIN
|
|
21200
|
+
NEW.currency := v_unit_cur;
|
|
21201
|
+
|
|
21202
|
+
IF NEW.destination_location_id IS NOT NULL THEN
|
|
21203
|
+
SELECT l.unit_id INTO v_dst_unit FROM public.plg_inventory_stock_locations l WHERE l.id = NEW.destination_location_id;
|
|
21204
|
+
IF app.unit_currency(NEW.tenant_id, v_dst_unit) <> v_unit_cur THEN
|
|
21205
|
+
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.',
|
|
21206
|
+
v_unit_cur, app.unit_currency(NEW.tenant_id, v_dst_unit)
|
|
21207
|
+
USING ERRCODE = '0A000';
|
|
21208
|
+
END IF;
|
|
21209
|
+
END IF;
|
|
21210
|
+
|
|
21211
|
+
IF v_src IS NULL OR v_src = v_unit_cur THEN
|
|
21212
|
+
NEW.source_currency := NULL; NEW.source_unit_cost := NULL; NEW.fx_rate := NULL;
|
|
21213
|
+
RETURN NEW;
|
|
21214
|
+
END IF;
|
|
21215
|
+
|
|
21216
|
+
NEW.source_currency := v_src;
|
|
21217
|
+
NEW.source_unit_cost := coalesce(NEW.source_unit_cost, NEW.unit_cost, 0);
|
|
21218
|
+
-- The tenant's rate table belongs to financial, which may not be installed.
|
|
21219
|
+
IF NEW.fx_rate IS NULL AND to_regprocedure('public.plg_financial_fx_rate(uuid,text,text,date)') IS NOT NULL THEN
|
|
21220
|
+
EXECUTE 'SELECT public.plg_financial_fx_rate($1, $2, $3, $4)'
|
|
21221
|
+
INTO NEW.fx_rate USING NEW.tenant_id, v_src, v_unit_cur, NEW.movement_date;
|
|
21222
|
+
END IF;
|
|
21223
|
+
IF NEW.fx_rate IS NULL OR NEW.fx_rate <= 0 THEN
|
|
21224
|
+
RAISE EXCEPTION 'Informe a taxa de c\xE2mbio de % para % desta entrada (ou cadastre a taxa do dia).', v_src, v_unit_cur
|
|
21225
|
+
USING ERRCODE = '22023';
|
|
21226
|
+
END IF;
|
|
21227
|
+
NEW.unit_cost := round(NEW.source_unit_cost * NEW.fx_rate, 4);
|
|
21228
|
+
NEW.total_cost := abs(NEW.quantity) * NEW.unit_cost;
|
|
21229
|
+
RETURN NEW;
|
|
21230
|
+
END $function$;
|
|
21231
|
+
|
|
21232
|
+
-- Named to fire after \`plg_inventory_stock_movements_before_insert\`, which
|
|
21233
|
+
-- stamps the unit and the cost this converts.
|
|
21234
|
+
DROP TRIGGER IF EXISTS plg_inventory_stock_movements_currency ON public.plg_inventory_stock_movements;
|
|
21235
|
+
CREATE TRIGGER plg_inventory_stock_movements_currency
|
|
21236
|
+
BEFORE INSERT ON public.plg_inventory_stock_movements
|
|
21237
|
+
FOR EACH ROW EXECUTE FUNCTION app.inventory_movements_currency();
|
|
21238
|
+
|
|
21239
|
+
CREATE OR REPLACE FUNCTION app.inventory_positions_currency()
|
|
21240
|
+
RETURNS trigger
|
|
21241
|
+
LANGUAGE plpgsql
|
|
21242
|
+
SECURITY DEFINER
|
|
21243
|
+
SET search_path TO ''
|
|
21244
|
+
AS $function$
|
|
21245
|
+
DECLARE v_unit uuid;
|
|
21246
|
+
BEGIN
|
|
21247
|
+
SELECT l.unit_id INTO v_unit FROM public.plg_inventory_stock_locations l WHERE l.id = NEW.stock_location_id;
|
|
21248
|
+
NEW.currency := app.unit_currency(NEW.tenant_id, coalesce(v_unit, NEW.unit_id));
|
|
21249
|
+
RETURN NEW;
|
|
21250
|
+
END $function$;
|
|
21251
|
+
DROP TRIGGER IF EXISTS plg_inventory_stock_positions_currency ON public.plg_inventory_stock_positions;
|
|
21252
|
+
CREATE TRIGGER plg_inventory_stock_positions_currency
|
|
21253
|
+
BEFORE INSERT ON public.plg_inventory_stock_positions
|
|
21254
|
+
FOR EACH ROW EXECUTE FUNCTION app.inventory_positions_currency();
|
|
21255
|
+
|
|
21256
|
+
-- The receipt passes the document's currency through. Patched in place: the
|
|
21257
|
+
-- live body differs from the file that created it.
|
|
21258
|
+
DO $patch$
|
|
21259
|
+
DECLARE
|
|
21260
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21261
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21262
|
+
v_cols constant text := 'idempotency_key, line_no, operation_id, metadata)';
|
|
21263
|
+
v_vals constant text := 'v_key, v_no, v_op.op_id, v_ln.declared)';
|
|
21264
|
+
BEGIN
|
|
21265
|
+
IF position('source_currency' in v_def) > 0 THEN
|
|
21266
|
+
RETURN; -- already passes it (a replay, or a later rewrite)
|
|
21267
|
+
END IF;
|
|
21268
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21269
|
+
RAISE EXCEPTION '026: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21270
|
+
END IF;
|
|
21271
|
+
v_def := overlay(v_def PLACING 'idempotency_key, line_no, operation_id, metadata, source_currency, fx_rate)'
|
|
21272
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21273
|
+
v_def := overlay(v_def PLACING
|
|
21274
|
+
'v_key, v_no, v_op.op_id, v_ln.declared,' || E'\\n'
|
|
21275
|
+
|| ' nullif(upper(btrim(coalesce(v_line->>''currency'', v_doc->>''currency'', ''''))), ''''),' || E'\\n'
|
|
21276
|
+
|| ' coalesce(nullif(v_line->>''fx_rate'', '''')::numeric, nullif(v_doc->>''fx_rate'', '''')::numeric))'
|
|
21277
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21278
|
+
EXECUTE v_def;
|
|
21279
|
+
END $patch$;
|
|
21280
|
+
`;
|
|
21281
|
+
var MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = `-- ============================================================================
|
|
21282
|
+
-- 027 \u2014 a receipt names the line it came from
|
|
21283
|
+
--
|
|
21284
|
+
-- Stock received because of a bill had no tie to the bill line: the ledger
|
|
21285
|
+
-- kept \`source_item_type\` / \`source_item_id\` columns, but \`inventory_receive\`
|
|
21286
|
+
-- never filled them, so "did this line enter stock?" was answered by guessing
|
|
21287
|
+
-- from the invoice number. A line of the receipt can now say where it came
|
|
21288
|
+
-- from, and the receipt writes it down.
|
|
21289
|
+
--
|
|
21290
|
+
-- Patched in place, as 026 did: the live body differs from the files.
|
|
21291
|
+
-- ============================================================================
|
|
21292
|
+
|
|
21293
|
+
DO $patch$
|
|
21294
|
+
DECLARE
|
|
21295
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21296
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21297
|
+
v_cols constant text := 'metadata, source_currency, fx_rate)';
|
|
21298
|
+
v_vals constant text := 'nullif(v_doc->>''fx_rate'', '''')::numeric))';
|
|
21299
|
+
BEGIN
|
|
21300
|
+
IF position('source_item_id' in v_def) > 0 THEN
|
|
21301
|
+
RETURN; -- already writes it (a replay, or a later rewrite)
|
|
21302
|
+
END IF;
|
|
21303
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21304
|
+
RAISE EXCEPTION '027: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21305
|
+
END IF;
|
|
21306
|
+
v_def := overlay(v_def PLACING 'metadata, source_currency, fx_rate, source_item_type, source_item_id)'
|
|
21307
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21308
|
+
v_def := overlay(v_def PLACING
|
|
21309
|
+
'nullif(v_doc->>''fx_rate'', '''')::numeric),' || E'\\n'
|
|
21310
|
+
|| ' nullif(v_line->>''source_item_type'', ''''), nullif(v_line->>''source_item_id'', '''')::uuid)'
|
|
21311
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21312
|
+
EXECUTE v_def;
|
|
21313
|
+
END $patch$;
|
|
21314
|
+
|
|
21315
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_stock_movements_source_item_idx
|
|
21316
|
+
ON public.plg_inventory_stock_movements (tenant_id, source_item_id) WHERE source_item_id IS NOT NULL;
|
|
21317
|
+
`;
|
|
21318
|
+
var MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = `-- ============================================================================
|
|
21319
|
+
-- 028 \u2014 a receipt keeps the variant
|
|
21320
|
+
--
|
|
21321
|
+
-- \`inventory_receive\` read \`variant_id\` off each line, checked it belonged to
|
|
21322
|
+
-- the product, and then did not write it: every receipt landed on the
|
|
21323
|
+
-- product's default variant. A shirt received as 10 M and 9 G was 19 of
|
|
21324
|
+
-- "Padr\xE3o". The line's variant now reaches the ledger row, which is what the
|
|
21325
|
+
-- per-variant balance is computed from.
|
|
21326
|
+
--
|
|
21327
|
+
-- Patched in place, as 026 and 027 did.
|
|
21328
|
+
--
|
|
21329
|
+
-- The column itself was never in this plugin's chain (022 says so): pools got
|
|
21330
|
+
-- it by hand, a database built from zero did not. It is declared here, in the
|
|
21331
|
+
-- shape the pools have. The value is read off the line's JSON, which exists in
|
|
21332
|
+
-- every version of the function; \`v_ln.variant_id\` only exists where the
|
|
21333
|
+
-- parser was widened by hand.
|
|
21334
|
+
-- ============================================================================
|
|
21335
|
+
|
|
21336
|
+
ALTER TABLE public.plg_inventory_stock_movements
|
|
21337
|
+
ADD COLUMN IF NOT EXISTS variant_id uuid REFERENCES public.product_variants(id) ON DELETE SET NULL;
|
|
21338
|
+
CREATE INDEX IF NOT EXISTS plg_inventory_stock_movements_variant_idx
|
|
21339
|
+
ON public.plg_inventory_stock_movements (tenant_id, variant_id) WHERE variant_id IS NOT NULL;
|
|
21340
|
+
|
|
21341
|
+
DO $patch$
|
|
21342
|
+
DECLARE
|
|
21343
|
+
v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;
|
|
21344
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21345
|
+
v_cols constant text := 'source_item_type, source_item_id)';
|
|
21346
|
+
v_vals constant text := 'nullif(v_line->>''source_item_id'', '''')::uuid)';
|
|
21347
|
+
BEGIN
|
|
21348
|
+
IF position('source_item_type, source_item_id, variant_id)' in v_def) > 0 THEN
|
|
21349
|
+
RETURN; -- already writes it (a replay, or a later rewrite)
|
|
21350
|
+
END IF;
|
|
21351
|
+
IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN
|
|
21352
|
+
RAISE EXCEPTION '028: inventory_receive no longer has the INSERT this extends; patch it by hand';
|
|
21353
|
+
END IF;
|
|
21354
|
+
v_def := overlay(v_def PLACING 'source_item_type, source_item_id, variant_id)'
|
|
21355
|
+
FROM position(v_cols in v_def) FOR length(v_cols));
|
|
21356
|
+
v_def := overlay(v_def PLACING 'nullif(v_line->>''source_item_id'', '''')::uuid, nullif(v_line->>''variant_id'', '''')::uuid)'
|
|
21357
|
+
FROM position(v_vals in v_def) FOR length(v_vals));
|
|
21358
|
+
EXECUTE v_def;
|
|
21359
|
+
END $patch$;
|
|
21360
|
+
`;
|
|
21361
|
+
var MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = `-- ============================================================================
|
|
21362
|
+
-- 029 \u2014 a reversal undoes the same variant
|
|
21363
|
+
--
|
|
21364
|
+
-- 028 made a receipt keep the variant it was for. \`inventory_reverse\` copies
|
|
21365
|
+
-- the row it undoes and left the variant out, so undoing "6 of M" tried to
|
|
21366
|
+
-- take 6 from the product's default variant \u2014 which has none, and the
|
|
21367
|
+
-- reversal was refused ("would take variant \u2026 below zero"). A receipt by size
|
|
21368
|
+
-- could be made and never corrected.
|
|
21369
|
+
--
|
|
21370
|
+
-- The reversal now carries the variant of the row it reverses.
|
|
21371
|
+
-- Patched in place, like 026\u2013028.
|
|
21372
|
+
-- ============================================================================
|
|
21373
|
+
|
|
21374
|
+
DO $patch$
|
|
21375
|
+
DECLARE
|
|
21376
|
+
v_fn regprocedure := 'public.inventory_reverse(text, text)'::regprocedure;
|
|
21377
|
+
v_def text := pg_get_functiondef(v_fn);
|
|
21378
|
+
v_new text;
|
|
21379
|
+
-- The function has two INSERTs (one per branch); both copy the row they undo.
|
|
21380
|
+
v_cols constant text := 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata)';
|
|
21381
|
+
v_vals1 constant text := 'jsonb_build_object(''reversed_movement_id'', r.id))';
|
|
21382
|
+
v_vals2 constant text := '''reversed_kind'', r.kind))';
|
|
21383
|
+
BEGIN
|
|
21384
|
+
IF position('r.variant_id' in v_def) > 0 THEN
|
|
21385
|
+
RETURN; -- already carries it (a replay, or a later rewrite)
|
|
21386
|
+
END IF;
|
|
21387
|
+
v_new := replace(v_def, v_cols, 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata, variant_id)');
|
|
21388
|
+
v_new := replace(v_new, v_vals1, 'jsonb_build_object(''reversed_movement_id'', r.id), r.variant_id)');
|
|
21389
|
+
v_new := replace(v_new, v_vals2, '''reversed_kind'', r.kind), r.variant_id)');
|
|
21390
|
+
-- Every INSERT that gained the column must have gained the value.
|
|
21391
|
+
IF (length(v_new) - length(replace(v_new, 'metadata, variant_id)', ''))) / length('metadata, variant_id)')
|
|
21392
|
+
<> (length(v_new) - length(replace(v_new, ', r.variant_id)', ''))) / length(', r.variant_id)')
|
|
21393
|
+
OR position('metadata, variant_id)' in v_new) = 0 THEN
|
|
21394
|
+
RAISE EXCEPTION '029: inventory_reverse no longer has the INSERTs this extends; patch it by hand';
|
|
21395
|
+
END IF;
|
|
21396
|
+
EXECUTE v_new;
|
|
21397
|
+
END $patch$;
|
|
21398
|
+
`;
|
|
20877
21399
|
var MIGRATIONS = [
|
|
20878
21400
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
20879
21401
|
{ id: "001_the_usage_that_left_stock_can_be_audited", sql: MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED },
|
|
@@ -20900,7 +21422,11 @@ var MIGRATIONS = [
|
|
|
20900
21422
|
{ id: "022_receber_estoque_volta_a_funcionar", sql: MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR },
|
|
20901
21423
|
{ id: "023_a_receita_se_escreve_com_permissao", sql: MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO },
|
|
20902
21424
|
{ 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 }
|
|
21425
|
+
{ id: "025_a_ficha_tecnica_por_estacao", sql: MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO },
|
|
21426
|
+
{ id: "026_stock_cost_knows_its_currency", sql: MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY },
|
|
21427
|
+
{ id: "027_a_receipt_names_the_line_it_came_from", sql: MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM },
|
|
21428
|
+
{ id: "028_a_receipt_keeps_the_variant", sql: MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT },
|
|
21429
|
+
{ id: "029_a_reversal_undoes_the_same_variant", sql: MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT }
|
|
20904
21430
|
];
|
|
20905
21431
|
|
|
20906
21432
|
// src/index.ts
|
|
@@ -20940,6 +21466,7 @@ function resolveConfig(options) {
|
|
|
20940
21466
|
locations: options?.locations ?? [],
|
|
20941
21467
|
moduleNav: options?.moduleNav,
|
|
20942
21468
|
extraViews: options?.extraViews ?? [],
|
|
21469
|
+
productDetailTabs: options?.productDetailTabs ?? [],
|
|
20943
21470
|
documentModal: options?.documentModal
|
|
20944
21471
|
};
|
|
20945
21472
|
}
|
|
@@ -20955,7 +21482,7 @@ function createInventoryPlugin(options) {
|
|
|
20955
21482
|
const provider = options?.dataProvider ?? createInventoryDataProvider();
|
|
20956
21483
|
const store = createInventoryStore(provider);
|
|
20957
21484
|
const dashboardWidgets = createInventoryDashboardWidgets({ config, provider, store });
|
|
20958
|
-
const PageComponent = () =>
|
|
21485
|
+
const PageComponent = () => React14.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
|
|
20959
21486
|
return {
|
|
20960
21487
|
id: "inventory",
|
|
20961
21488
|
// Como esta conta refaz o módulo com as opções DELA. Ver PluginRebuild:
|
|
@@ -21124,9 +21651,9 @@ function createInventoryPlugin(options) {
|
|
|
21124
21651
|
// Categories, Units and Suppliers all existed in code and appeared
|
|
21125
21652
|
// nowhere. financial and crm already render through it.
|
|
21126
21653
|
component: (() => {
|
|
21127
|
-
const Tab = () =>
|
|
21654
|
+
const Tab = () => React14.createElement(PluginSettingsPanel, {
|
|
21128
21655
|
title: config.labels.pageTitle,
|
|
21129
|
-
generalSettings:
|
|
21656
|
+
generalSettings: React14.createElement(InventoryGeneralSettings, { config }),
|
|
21130
21657
|
registries: inventoryRegistries,
|
|
21131
21658
|
hostPluginId: "inventory",
|
|
21132
21659
|
routeBase: "/settings/inventory"
|