@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-4849712ccb
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/generated/entities/catalog_product/index.js +16 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
- package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
- package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
- package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
- package/dist/generated/entities/sales_invoice_line/index.js +7 -1
- package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +6 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entities/sales_quote_line/index.js +6 -0
- package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +123 -8
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
- package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +351 -201
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/index.js +1 -0
- package/dist/modules/catalog/commands/index.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
- package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +355 -73
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +18 -4
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
- package/dist/modules/catalog/components/products/productForm.js +66 -5
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
- package/dist/modules/catalog/data/entities.js +86 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +65 -3
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/events.js +3 -0
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/lib/unitCodes.js +7 -0
- package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
- package/dist/modules/catalog/lib/unitResolution.js +53 -0
- package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
- package/dist/modules/catalog/search.js +69 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +91 -42
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/dashboards/seed/analytics.js +3 -0
- package/dist/modules/dashboards/seed/analytics.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +98 -15
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +101 -14
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1424 -260
- package/dist/modules/sales/commands/documents.js.map +3 -3
- package/dist/modules/sales/commands/shared.js +6 -2
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +59 -3
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +35 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
- package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
- package/dist/modules/sales/search.js +28 -0
- package/dist/modules/sales/search.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +14 -1
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +8 -0
- package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
- package/generated/entities/sales_credit_memo_line/index.ts +3 -0
- package/generated/entities/sales_invoice_line/index.ts +3 -0
- package/generated/entities/sales_order_line/index.ts +3 -0
- package/generated/entities/sales_quote_line/index.ts +3 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/catalog/api/prices/route.ts +213 -81
- package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
- package/src/modules/catalog/api/products/route.ts +638 -402
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
- package/src/modules/catalog/commands/index.ts +1 -0
- package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
- package/src/modules/catalog/commands/products.ts +1151 -693
- package/src/modules/catalog/commands/shared.ts +19 -5
- package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
- package/src/modules/catalog/components/products/productForm.ts +369 -256
- package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
- package/src/modules/catalog/data/entities.ts +82 -1
- package/src/modules/catalog/data/validators.ts +118 -34
- package/src/modules/catalog/events.ts +3 -0
- package/src/modules/catalog/i18n/de.json +56 -0
- package/src/modules/catalog/i18n/en.json +56 -0
- package/src/modules/catalog/i18n/es.json +56 -0
- package/src/modules/catalog/i18n/pl.json +56 -0
- package/src/modules/catalog/lib/unitCodes.ts +1 -0
- package/src/modules/catalog/lib/unitResolution.ts +62 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
- package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
- package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
- package/src/modules/catalog/search.ts +73 -1
- package/src/modules/catalog/seed/examples.ts +552 -479
- package/src/modules/dashboards/i18n/de.json +1 -1
- package/src/modules/dashboards/i18n/en.json +1 -1
- package/src/modules/dashboards/i18n/es.json +1 -1
- package/src/modules/dashboards/i18n/pl.json +1 -1
- package/src/modules/dashboards/seed/analytics.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +158 -68
- package/src/modules/sales/api/quote-lines/route.ts +161 -67
- package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
- package/src/modules/sales/commands/documents.ts +4250 -2424
- package/src/modules/sales/commands/shared.ts +7 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
- package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
- package/src/modules/sales/data/entities.ts +53 -0
- package/src/modules/sales/data/validators.ts +36 -0
- package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
- package/src/modules/sales/i18n/de.json +23 -3
- package/src/modules/sales/i18n/en.json +23 -3
- package/src/modules/sales/i18n/es.json +23 -3
- package/src/modules/sales/i18n/pl.json +23 -3
- package/src/modules/sales/lib/types.ts +30 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
- package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
- package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
- package/src/modules/sales/search.ts +28 -0
- package/src/modules/sales/seed/examples.ts +20 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
- package/src/modules/workflows/i18n/de.json +4 -4
- package/src/modules/workflows/i18n/en.json +4 -4
- package/src/modules/workflows/i18n/es.json +4 -4
- package/src/modules/workflows/i18n/pl.json +4 -4
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
LookupSelect
|
|
6
|
+
} from "@open-mercato/ui/backend/inputs";
|
|
5
7
|
import {
|
|
6
8
|
CrudForm
|
|
7
9
|
} from "@open-mercato/ui/backend/CrudForm";
|
|
@@ -9,7 +11,12 @@ import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customF
|
|
|
9
11
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
12
|
import { createCrud, updateCrud } from "@open-mercato/ui/backend/utils/crud";
|
|
11
13
|
import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle
|
|
19
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
13
20
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
14
21
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
15
22
|
import { DollarSign, Settings } from "lucide-react";
|
|
@@ -22,12 +29,17 @@ import { E } from "../../../../generated/entities.ids.generated.js";
|
|
|
22
29
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
23
30
|
import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
24
31
|
import { formatMoney, normalizeNumber } from "./lineItemUtils.js";
|
|
25
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
normalizeCustomFieldSubmitValue,
|
|
34
|
+
extractCustomFieldValues
|
|
35
|
+
} from "./customFieldHelpers.js";
|
|
36
|
+
import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
|
|
26
37
|
const defaultForm = (currencyCode) => ({
|
|
27
38
|
lineMode: "catalog",
|
|
28
39
|
productId: null,
|
|
29
40
|
variantId: null,
|
|
30
41
|
quantity: "1",
|
|
42
|
+
quantityUnit: null,
|
|
31
43
|
priceId: null,
|
|
32
44
|
priceMode: "gross",
|
|
33
45
|
unitPrice: "",
|
|
@@ -39,25 +51,28 @@ const defaultForm = (currencyCode) => ({
|
|
|
39
51
|
customFieldSetId: null,
|
|
40
52
|
statusEntryId: null
|
|
41
53
|
});
|
|
54
|
+
const UNIT_PRICE_INPUT_SCALE = 4;
|
|
42
55
|
function buildPriceScopeReason(item, t) {
|
|
43
56
|
const tags = [];
|
|
44
57
|
const add = (key) => tags.push(key);
|
|
45
|
-
if (item.channel_id || item.channelId)
|
|
46
|
-
|
|
47
|
-
if (item.
|
|
48
|
-
|
|
49
|
-
if (item.
|
|
50
|
-
|
|
51
|
-
if (item.
|
|
58
|
+
if (item.channel_id || item.channelId)
|
|
59
|
+
add(t("sales.documents.items.priceScope.channel", "Channel"));
|
|
60
|
+
if (item.offer_id || item.offerId)
|
|
61
|
+
add(t("sales.documents.items.priceScope.offer", "Offer"));
|
|
62
|
+
if (item.variant_id || item.variantId)
|
|
63
|
+
add(t("sales.documents.items.priceScope.variant", "Variant"));
|
|
64
|
+
if (item.customer_group_id || item.customerGroupId)
|
|
65
|
+
add(t("sales.documents.items.priceScope.customerGroup", "Customer group"));
|
|
66
|
+
if (item.customer_id || item.customerId)
|
|
67
|
+
add(t("sales.documents.items.priceScope.customer", "Customer"));
|
|
68
|
+
if (item.user_group_id || item.userGroupId)
|
|
69
|
+
add(t("sales.documents.items.priceScope.userGroup", "User group"));
|
|
70
|
+
if (item.user_id || item.userId)
|
|
71
|
+
add(t("sales.documents.items.priceScope.user", "User"));
|
|
52
72
|
const minQty = normalizeNumber(item.min_quantity, Number.NaN);
|
|
53
73
|
const maxQty = normalizeNumber(item.max_quantity, Number.NaN);
|
|
54
74
|
if (Number.isFinite(minQty) || Number.isFinite(maxQty)) {
|
|
55
|
-
add(
|
|
56
|
-
t(
|
|
57
|
-
"sales.documents.items.priceScope.quantity",
|
|
58
|
-
"Quantity"
|
|
59
|
-
)
|
|
60
|
-
);
|
|
75
|
+
add(t("sales.documents.items.priceScope.quantity", "Quantity"));
|
|
61
76
|
}
|
|
62
77
|
if (item.starts_at || item.ends_at) {
|
|
63
78
|
add(t("sales.documents.items.priceScope.schedule", "Scheduled"));
|
|
@@ -68,6 +83,49 @@ function buildPriceScopeReason(item, t) {
|
|
|
68
83
|
function buildPlaceholder(label) {
|
|
69
84
|
return /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded border bg-muted text-[10px] uppercase text-muted-foreground", children: (label ?? "").slice(0, 2) || "\u2022" });
|
|
70
85
|
}
|
|
86
|
+
function normalizeUnitCode(value) {
|
|
87
|
+
return canonicalizeUnitCode(value);
|
|
88
|
+
}
|
|
89
|
+
function getRecordBoolean(record, fallback, ...keys) {
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const val = record[key];
|
|
92
|
+
if (typeof val === "boolean") return val;
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
function getUomProductFields(item) {
|
|
97
|
+
return {
|
|
98
|
+
defaultUnit: normalizeUnitCode(item.default_unit ?? item.defaultUnit),
|
|
99
|
+
defaultSalesUnit: normalizeUnitCode(
|
|
100
|
+
item.default_sales_unit ?? item.defaultSalesUnit
|
|
101
|
+
),
|
|
102
|
+
defaultSalesUnitQuantity: normalizeNumber(
|
|
103
|
+
item.default_sales_unit_quantity ?? item.defaultSalesUnitQuantity,
|
|
104
|
+
Number.NaN
|
|
105
|
+
)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function getUomConversionFields(row) {
|
|
109
|
+
return {
|
|
110
|
+
unitCode: normalizeUnitCode(row.unit_code ?? row.unitCode),
|
|
111
|
+
isActive: getRecordBoolean(row, true, "is_active", "isActive"),
|
|
112
|
+
toBaseFactor: normalizeNumber(
|
|
113
|
+
row.to_base_factor ?? row.toBaseFactor,
|
|
114
|
+
Number.NaN
|
|
115
|
+
)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function normalizeQuantityPreview(value) {
|
|
119
|
+
if (!Number.isFinite(value)) return 0;
|
|
120
|
+
return Math.round(value * 1e6) / 1e6;
|
|
121
|
+
}
|
|
122
|
+
function normalizeUnitPriceInputValue(value) {
|
|
123
|
+
if (!Number.isFinite(value)) return "";
|
|
124
|
+
const factor = 10 ** UNIT_PRICE_INPUT_SCALE;
|
|
125
|
+
const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
|
|
126
|
+
if (!Number.isFinite(rounded)) return "";
|
|
127
|
+
return rounded.toString();
|
|
128
|
+
}
|
|
71
129
|
function LineItemDialog({
|
|
72
130
|
open,
|
|
73
131
|
kind,
|
|
@@ -83,8 +141,12 @@ function LineItemDialog({
|
|
|
83
141
|
const scope = useOrganizationScopeDetail();
|
|
84
142
|
const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null;
|
|
85
143
|
const resolvedTenantId = tenantId ?? scope.tenantId ?? null;
|
|
86
|
-
const [initialValues, setInitialValues] = React.useState(
|
|
87
|
-
|
|
144
|
+
const [initialValues, setInitialValues] = React.useState(
|
|
145
|
+
() => defaultForm(currencyCode)
|
|
146
|
+
);
|
|
147
|
+
const [lineMode, setLineMode] = React.useState(
|
|
148
|
+
defaultForm(currencyCode).lineMode
|
|
149
|
+
);
|
|
88
150
|
const [productOption, setProductOption] = React.useState(null);
|
|
89
151
|
const [variantOption, setVariantOption] = React.useState(null);
|
|
90
152
|
const [priceOptions, setPriceOptions] = React.useState([]);
|
|
@@ -93,6 +155,7 @@ function LineItemDialog({
|
|
|
93
155
|
const [editingId, setEditingId] = React.useState(null);
|
|
94
156
|
const [taxRates, setTaxRates] = React.useState([]);
|
|
95
157
|
const [lineStatuses, setLineStatuses] = React.useState([]);
|
|
158
|
+
const [unitOptions, setUnitOptions] = React.useState([]);
|
|
96
159
|
const [, setLineStatusLoading] = React.useState(false);
|
|
97
160
|
const productOptionsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
98
161
|
const variantOptionsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
@@ -157,6 +220,7 @@ function LineItemDialog({
|
|
|
157
220
|
setProductOption(null);
|
|
158
221
|
setVariantOption(null);
|
|
159
222
|
setPriceOptions([]);
|
|
223
|
+
setUnitOptions([]);
|
|
160
224
|
setEditingId(null);
|
|
161
225
|
setFormResetKey((prev) => prev + 1);
|
|
162
226
|
},
|
|
@@ -168,20 +232,26 @@ function LineItemDialog({
|
|
|
168
232
|
}, [onOpenChange, resetForm]);
|
|
169
233
|
const loadTaxRates = React.useCallback(async () => {
|
|
170
234
|
try {
|
|
171
|
-
const response = await apiCall(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{ fallback: { items: [] } }
|
|
175
|
-
);
|
|
235
|
+
const response = await apiCall("/api/sales/tax-rates?pageSize=100", void 0, {
|
|
236
|
+
fallback: { items: [] }
|
|
237
|
+
});
|
|
176
238
|
const items = Array.isArray(response.result?.items) ? response.result.items : [];
|
|
177
239
|
const parsed = items.map((item) => {
|
|
178
240
|
const id = typeof item.id === "string" ? item.id : null;
|
|
179
241
|
const name = typeof item.name === "string" && item.name.trim().length ? item.name.trim() : typeof item.code === "string" ? item.code : null;
|
|
180
242
|
if (!id || !name) return null;
|
|
181
243
|
const rate = normalizeNumber(item.rate);
|
|
182
|
-
const code = typeof item.code === "string" && item.code
|
|
183
|
-
const isDefault = Boolean(
|
|
184
|
-
|
|
244
|
+
const code = typeof item.code === "string" && item.code?.trim().length ? item.code?.trim() ?? null : null;
|
|
245
|
+
const isDefault = Boolean(
|
|
246
|
+
item.isDefault ?? item.is_default
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
name,
|
|
251
|
+
code,
|
|
252
|
+
rate: Number.isFinite(rate) ? rate : null,
|
|
253
|
+
isDefault
|
|
254
|
+
};
|
|
185
255
|
}).filter((entry) => Boolean(entry));
|
|
186
256
|
taxRatesRef.current = parsed;
|
|
187
257
|
defaultTaxRateRef.current = parsed.find((rate) => rate.isDefault) ?? null;
|
|
@@ -199,44 +269,63 @@ function LineItemDialog({
|
|
|
199
269
|
async (query) => {
|
|
200
270
|
const params = new URLSearchParams({ pageSize: "8" });
|
|
201
271
|
if (query && query.trim().length) params.set("search", query.trim());
|
|
202
|
-
const response = await apiCall(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
{ fallback: { items: [] } }
|
|
206
|
-
);
|
|
272
|
+
const response = await apiCall(`/api/catalog/products?${params.toString()}`, void 0, {
|
|
273
|
+
fallback: { items: [] }
|
|
274
|
+
});
|
|
207
275
|
const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : [];
|
|
208
276
|
const needle = query?.trim().toLowerCase() ?? "";
|
|
209
277
|
return items.map((item) => {
|
|
210
278
|
const id = typeof item.id === "string" ? item.id : null;
|
|
211
279
|
if (!id) return null;
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
280
|
+
const productItem = item;
|
|
281
|
+
const title = typeof item.title === "string" ? item.title : typeof productItem.name === "string" ? productItem.name : id;
|
|
282
|
+
const sku = typeof productItem.sku === "string" ? productItem.sku : null;
|
|
283
|
+
const thumbnail = typeof productItem.default_media_url === "string" ? productItem.default_media_url : typeof productItem.defaultMediaUrl === "string" ? productItem.defaultMediaUrl : null;
|
|
284
|
+
const pricing = typeof productItem.pricing === "object" && productItem.pricing ? productItem.pricing : null;
|
|
285
|
+
const metadata = typeof productItem.metadata === "object" && productItem.metadata ? productItem.metadata : null;
|
|
286
|
+
const pricingMeta = pricing;
|
|
287
|
+
const metaMeta = metadata;
|
|
288
|
+
const pricingTaxRateId = typeof pricingMeta?.tax_rate_id === "string" && pricingMeta.tax_rate_id.trim().length ? pricingMeta.tax_rate_id.trim() : typeof pricingMeta?.taxRateId === "string" && pricingMeta.taxRateId.trim().length ? pricingMeta.taxRateId.trim() : null;
|
|
289
|
+
const metaTaxRateId = typeof metaMeta?.taxRateId === "string" && metaMeta.taxRateId.trim().length ? metaMeta.taxRateId.trim() : typeof metaMeta?.tax_rate_id === "string" && metaMeta.tax_rate_id.trim().length ? metaMeta.tax_rate_id.trim() : null;
|
|
219
290
|
const taxRateValue = normalizeNumber(
|
|
220
|
-
|
|
291
|
+
pricingMeta?.tax_rate ?? pricingMeta?.taxRate ?? productItem.tax_rate ?? productItem.taxRate,
|
|
221
292
|
Number.NaN
|
|
222
293
|
);
|
|
294
|
+
const uomFields = getUomProductFields(item);
|
|
295
|
+
const defaultUnit = uomFields.defaultUnit;
|
|
296
|
+
const defaultSalesUnit = uomFields.defaultSalesUnit;
|
|
297
|
+
const defaultSalesUnitQuantity = uomFields.defaultSalesUnitQuantity;
|
|
223
298
|
const matches = !needle || title.toLowerCase().includes(needle) || (sku ? sku.toLowerCase().includes(needle) : false);
|
|
224
299
|
if (!matches) return null;
|
|
225
300
|
return {
|
|
226
301
|
id,
|
|
227
302
|
title,
|
|
228
303
|
subtitle: sku ?? void 0,
|
|
229
|
-
icon: thumbnail ? /* @__PURE__ */ jsx(
|
|
304
|
+
icon: thumbnail ? /* @__PURE__ */ jsx(
|
|
305
|
+
"img",
|
|
306
|
+
{
|
|
307
|
+
src: thumbnail,
|
|
308
|
+
alt: title,
|
|
309
|
+
className: "h-8 w-8 rounded object-cover"
|
|
310
|
+
}
|
|
311
|
+
) : buildPlaceholder(title),
|
|
230
312
|
option: {
|
|
231
313
|
id,
|
|
232
314
|
title,
|
|
233
315
|
sku,
|
|
234
316
|
thumbnailUrl: thumbnail,
|
|
235
317
|
taxRateId: pricingTaxRateId ?? metaTaxRateId ?? null,
|
|
236
|
-
taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null
|
|
318
|
+
taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null,
|
|
319
|
+
defaultUnit,
|
|
320
|
+
defaultSalesUnit,
|
|
321
|
+
defaultSalesUnitQuantity: Number.isFinite(
|
|
322
|
+
defaultSalesUnitQuantity
|
|
323
|
+
) ? defaultSalesUnitQuantity : null
|
|
237
324
|
}
|
|
238
325
|
};
|
|
239
|
-
}).filter(
|
|
326
|
+
}).filter(
|
|
327
|
+
(entry) => Boolean(entry)
|
|
328
|
+
).map((entry) => {
|
|
240
329
|
productOptionsRef.current.set(entry.option.id, entry.option);
|
|
241
330
|
return entry;
|
|
242
331
|
});
|
|
@@ -256,19 +345,28 @@ function LineItemDialog({
|
|
|
256
345
|
const id = typeof item.id === "string" ? item.id : null;
|
|
257
346
|
if (!id) return null;
|
|
258
347
|
const title = typeof item.name === "string" ? item.name : id;
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
348
|
+
const variantItem = item;
|
|
349
|
+
const sku = typeof variantItem.sku === "string" ? variantItem.sku : null;
|
|
350
|
+
const metadata = typeof variantItem.metadata === "object" && variantItem.metadata ? variantItem.metadata : null;
|
|
351
|
+
const variantMeta = metadata;
|
|
352
|
+
const variantTaxRateId = typeof variantMeta?.taxRateId === "string" && variantMeta.taxRateId.trim().length ? variantMeta.taxRateId.trim() : typeof variantMeta?.tax_rate_id === "string" && variantMeta.tax_rate_id.trim().length ? variantMeta.tax_rate_id.trim() : null;
|
|
262
353
|
const variantTaxRate = normalizeNumber(
|
|
263
|
-
|
|
354
|
+
variantItem.tax_rate ?? variantItem.taxRate ?? variantMeta?.tax_rate ?? variantMeta?.taxRate,
|
|
264
355
|
Number.NaN
|
|
265
356
|
);
|
|
266
|
-
const thumbnail = typeof
|
|
357
|
+
const thumbnail = typeof variantItem.default_media_url === "string" ? variantItem.default_media_url : typeof variantItem.thumbnailUrl === "string" ? variantItem.thumbnailUrl : fallbackThumbnail ?? null;
|
|
267
358
|
return {
|
|
268
359
|
id,
|
|
269
360
|
title,
|
|
270
361
|
subtitle: sku ?? void 0,
|
|
271
|
-
icon: thumbnail ? /* @__PURE__ */ jsx(
|
|
362
|
+
icon: thumbnail ? /* @__PURE__ */ jsx(
|
|
363
|
+
"img",
|
|
364
|
+
{
|
|
365
|
+
src: thumbnail,
|
|
366
|
+
alt: title,
|
|
367
|
+
className: "h-8 w-8 rounded object-cover"
|
|
368
|
+
}
|
|
369
|
+
) : buildPlaceholder(title),
|
|
272
370
|
option: {
|
|
273
371
|
id,
|
|
274
372
|
title,
|
|
@@ -278,15 +376,82 @@ function LineItemDialog({
|
|
|
278
376
|
taxRate: Number.isFinite(variantTaxRate) ? variantTaxRate : null
|
|
279
377
|
}
|
|
280
378
|
};
|
|
281
|
-
}).filter(
|
|
379
|
+
}).filter(
|
|
380
|
+
(entry) => Boolean(entry)
|
|
381
|
+
).map((entry) => {
|
|
282
382
|
variantOptionsRef.current.set(entry.option.id, entry.option);
|
|
283
383
|
return entry;
|
|
284
384
|
});
|
|
285
385
|
},
|
|
286
386
|
[]
|
|
287
387
|
);
|
|
388
|
+
const loadProductUnits = React.useCallback(
|
|
389
|
+
async (productId, option) => {
|
|
390
|
+
if (!productId) {
|
|
391
|
+
setUnitOptions([]);
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
const map = /* @__PURE__ */ new Map();
|
|
395
|
+
let baseUnit = normalizeUnitCode(option?.defaultUnit);
|
|
396
|
+
let defaultSalesUnit = normalizeUnitCode(option?.defaultSalesUnit);
|
|
397
|
+
if (!baseUnit || !defaultSalesUnit) {
|
|
398
|
+
try {
|
|
399
|
+
const response = await apiCall(
|
|
400
|
+
`/api/catalog/products?id=${encodeURIComponent(productId)}&pageSize=1`,
|
|
401
|
+
void 0,
|
|
402
|
+
{ fallback: { items: [] } }
|
|
403
|
+
);
|
|
404
|
+
const records = Array.isArray(response.result?.items) ? response.result.items : [];
|
|
405
|
+
const matched = records.find((entry) => entry.id === productId) ?? records[0] ?? null;
|
|
406
|
+
if (matched) {
|
|
407
|
+
const matchedUom = getUomProductFields(matched);
|
|
408
|
+
baseUnit = baseUnit ?? matchedUom.defaultUnit;
|
|
409
|
+
defaultSalesUnit = defaultSalesUnit ?? matchedUom.defaultSalesUnit;
|
|
410
|
+
}
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.error("sales.document.items.loadProductUnits.hydration", err);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (baseUnit) {
|
|
416
|
+
map.set(baseUnit, { code: baseUnit, toBaseFactor: 1, isBase: true });
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const response = await apiCall(
|
|
420
|
+
`/api/catalog/product-unit-conversions?productId=${encodeURIComponent(productId)}&pageSize=100`,
|
|
421
|
+
void 0,
|
|
422
|
+
{ fallback: { items: [] } }
|
|
423
|
+
);
|
|
424
|
+
const rows = Array.isArray(response.result?.items) ? response.result.items : [];
|
|
425
|
+
for (const row of rows) {
|
|
426
|
+
const conv = getUomConversionFields(row);
|
|
427
|
+
if (!conv.unitCode) continue;
|
|
428
|
+
if (!conv.isActive) continue;
|
|
429
|
+
map.set(conv.unitCode, {
|
|
430
|
+
code: conv.unitCode,
|
|
431
|
+
toBaseFactor: Number.isFinite(conv.toBaseFactor) && conv.toBaseFactor > 0 ? conv.toBaseFactor : null,
|
|
432
|
+
isBase: baseUnit ? conv.unitCode === baseUnit : false
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error("sales.document.items.loadUnits", err);
|
|
437
|
+
}
|
|
438
|
+
if (defaultSalesUnit && !map.has(defaultSalesUnit)) {
|
|
439
|
+
map.set(defaultSalesUnit, {
|
|
440
|
+
code: defaultSalesUnit,
|
|
441
|
+
toBaseFactor: baseUnit && defaultSalesUnit === baseUnit ? 1 : null,
|
|
442
|
+
isBase: baseUnit ? defaultSalesUnit === baseUnit : false
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const nextOptions = Array.from(map.values()).sort(
|
|
446
|
+
(left, right) => left.code.localeCompare(right.code)
|
|
447
|
+
);
|
|
448
|
+
setUnitOptions(nextOptions);
|
|
449
|
+
return nextOptions;
|
|
450
|
+
},
|
|
451
|
+
[]
|
|
452
|
+
);
|
|
288
453
|
const loadPrices = React.useCallback(
|
|
289
|
-
async (productId, variantId) => {
|
|
454
|
+
async (productId, variantId, quantity, quantityUnit) => {
|
|
290
455
|
if (!productId) {
|
|
291
456
|
setPriceOptions([]);
|
|
292
457
|
return [];
|
|
@@ -295,20 +460,38 @@ function LineItemDialog({
|
|
|
295
460
|
try {
|
|
296
461
|
const params = new URLSearchParams({ productId, pageSize: "20" });
|
|
297
462
|
if (variantId) params.set("variantId", variantId);
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
463
|
+
const quantityValue = normalizeNumber(quantity, Number.NaN);
|
|
464
|
+
if (Number.isFinite(quantityValue) && quantityValue > 0) {
|
|
465
|
+
params.set("quantity", String(quantityValue));
|
|
466
|
+
}
|
|
467
|
+
const quantityUnitCode = normalizeUnitCode(quantityUnit);
|
|
468
|
+
if (quantityUnitCode) {
|
|
469
|
+
params.set("quantityUnit", quantityUnitCode);
|
|
470
|
+
}
|
|
471
|
+
const response = await apiCall(`/api/catalog/prices?${params.toString()}`, void 0, {
|
|
472
|
+
fallback: { items: [] }
|
|
473
|
+
});
|
|
303
474
|
const items = Array.isArray(response.result?.items) ? response.result.items : [];
|
|
304
475
|
const mapped = items.map((item) => {
|
|
305
476
|
const id = typeof item.id === "string" ? item.id : null;
|
|
306
477
|
if (!id) return null;
|
|
307
|
-
const
|
|
308
|
-
|
|
478
|
+
const amountNetRaw = normalizeNumber(
|
|
479
|
+
item.unit_price_net,
|
|
480
|
+
Number.NaN
|
|
481
|
+
);
|
|
482
|
+
const amountGrossRaw = normalizeNumber(
|
|
483
|
+
item.unit_price_gross,
|
|
484
|
+
Number.NaN
|
|
485
|
+
);
|
|
486
|
+
const amountNet = Number.isFinite(amountNetRaw) ? amountNetRaw : null;
|
|
487
|
+
const amountGross = Number.isFinite(amountGrossRaw) ? amountGrossRaw : null;
|
|
309
488
|
const currency = typeof item.currency_code === "string" ? item.currency_code : typeof item.currencyCode === "string" ? item.currencyCode : null;
|
|
310
489
|
const displayMode = item.display_mode === "including-tax" || item.display_mode === "excluding-tax" ? item.display_mode : item.displayMode === "including-tax" || item.displayMode === "excluding-tax" ? item.displayMode : null;
|
|
311
|
-
const
|
|
490
|
+
const taxRateRaw = normalizeNumber(
|
|
491
|
+
item.tax_rate,
|
|
492
|
+
Number.NaN
|
|
493
|
+
);
|
|
494
|
+
const taxRate = Number.isFinite(taxRateRaw) ? taxRateRaw : null;
|
|
312
495
|
const priceKindId = typeof item.price_kind_id === "string" ? item.price_kind_id : typeof item.priceKindId === "string" ? item.priceKindId : null;
|
|
313
496
|
const priceKindTitle = typeof item.price_kind_title === "string" ? item.price_kind_title : typeof item.priceKindTitle === "string" ? item.priceKindTitle : typeof item.price_kind === "object" && item && typeof item.price_kind?.title === "string" ? item.price_kind.title : typeof item.price_kind === "object" && item && typeof item.price_kind?.name === "string" ? item.price_kind.name : null;
|
|
314
497
|
const priceKindCode = typeof item.price_kind_code === "string" ? item.price_kind_code : typeof item.priceKindCode === "string" ? item.priceKindCode : typeof item.price_kind === "object" && item && typeof item.price_kind?.code === "string" ? item.price_kind.code : null;
|
|
@@ -318,7 +501,10 @@ function LineItemDialog({
|
|
|
318
501
|
displayMode === "excluding-tax" && amountNet !== null && currency ? formatMoney(amountNet, currency) : null,
|
|
319
502
|
displayMode ? displayMode === "including-tax" ? t("sales.documents.items.priceGross", "Gross") : t("sales.documents.items.priceNet", "Net") : null
|
|
320
503
|
].filter(Boolean);
|
|
321
|
-
const { reason, tags } = buildPriceScopeReason(
|
|
504
|
+
const { reason, tags } = buildPriceScopeReason(
|
|
505
|
+
item,
|
|
506
|
+
(key, fallback) => t(key, fallback)
|
|
507
|
+
);
|
|
322
508
|
const label = labelParts.length > 0 ? labelParts.join(" \u2022 ") : amountGross !== null && currency ? formatMoney(amountGross, currency) : amountNet !== null && currency ? formatMoney(amountNet, currency) : id;
|
|
323
509
|
return {
|
|
324
510
|
id,
|
|
@@ -346,15 +532,85 @@ function LineItemDialog({
|
|
|
346
532
|
},
|
|
347
533
|
[t]
|
|
348
534
|
);
|
|
535
|
+
const selectPriceAfterRefresh = React.useCallback(
|
|
536
|
+
(prices, currentPriceId, currentPriceKindId) => {
|
|
537
|
+
if (!prices.length) return null;
|
|
538
|
+
if (currentPriceId) {
|
|
539
|
+
const sameId = prices.find((entry) => entry.id === currentPriceId);
|
|
540
|
+
if (sameId) return sameId;
|
|
541
|
+
}
|
|
542
|
+
if (currentPriceKindId) {
|
|
543
|
+
const sameKind = prices.find(
|
|
544
|
+
(entry) => entry.priceKindId === currentPriceKindId
|
|
545
|
+
);
|
|
546
|
+
if (sameKind) return sameKind;
|
|
547
|
+
}
|
|
548
|
+
return prices[0] ?? null;
|
|
549
|
+
},
|
|
550
|
+
[]
|
|
551
|
+
);
|
|
552
|
+
const resolveUnitPriceFactor = React.useCallback(
|
|
553
|
+
(quantityUnit) => {
|
|
554
|
+
const normalized = normalizeUnitCode(quantityUnit);
|
|
555
|
+
if (!normalized) return 1;
|
|
556
|
+
const unit = unitOptions.find((entry) => entry.code === normalized) ?? null;
|
|
557
|
+
const factor = normalizeNumber(unit?.toBaseFactor, Number.NaN);
|
|
558
|
+
if (!Number.isFinite(factor) || factor <= 0) return 1;
|
|
559
|
+
return factor;
|
|
560
|
+
},
|
|
561
|
+
[unitOptions]
|
|
562
|
+
);
|
|
563
|
+
const convertUnitPriceForUnitChange = React.useCallback(
|
|
564
|
+
(rawUnitPrice, fromUnit, toUnit) => {
|
|
565
|
+
const amount = normalizeNumber(rawUnitPrice, Number.NaN);
|
|
566
|
+
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
567
|
+
const fromCode = normalizeUnitCode(fromUnit);
|
|
568
|
+
const toCode = normalizeUnitCode(toUnit);
|
|
569
|
+
if (!fromCode || !toCode || fromCode === toCode) return null;
|
|
570
|
+
const fromFactor = resolveUnitPriceFactor(fromCode);
|
|
571
|
+
const toFactor = resolveUnitPriceFactor(toCode);
|
|
572
|
+
if (!Number.isFinite(fromFactor) || !Number.isFinite(toFactor) || fromFactor <= 0 || toFactor <= 0) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const baseAmount = amount / fromFactor;
|
|
576
|
+
const convertedAmount = baseAmount * toFactor;
|
|
577
|
+
if (!Number.isFinite(convertedAmount) || convertedAmount <= 0) return null;
|
|
578
|
+
return normalizeUnitPriceInputValue(convertedAmount);
|
|
579
|
+
},
|
|
580
|
+
[resolveUnitPriceFactor]
|
|
581
|
+
);
|
|
582
|
+
const applyPriceSelection = React.useCallback(
|
|
583
|
+
(selected, setFormValue, options) => {
|
|
584
|
+
if (!setFormValue) return;
|
|
585
|
+
if (selected) {
|
|
586
|
+
const mode = selected.displayMode === "excluding-tax" ? "net" : "gross";
|
|
587
|
+
const amountPerBaseUnit = mode === "net" ? selected.amountNet ?? selected.amountGross ?? 0 : selected.amountGross ?? selected.amountNet ?? 0;
|
|
588
|
+
const factor = resolveUnitPriceFactor(options?.quantityUnit ?? null);
|
|
589
|
+
const amount = Number.isFinite(amountPerBaseUnit * factor) ? amountPerBaseUnit * factor : amountPerBaseUnit;
|
|
590
|
+
setFormValue("priceId", selected.id);
|
|
591
|
+
setFormValue("priceMode", mode);
|
|
592
|
+
setFormValue("unitPrice", normalizeUnitPriceInputValue(amount));
|
|
593
|
+
setFormValue("taxRate", selected.taxRate ?? null);
|
|
594
|
+
setFormValue("taxRateId", findTaxRateIdByValue(selected.taxRate));
|
|
595
|
+
setFormValue(
|
|
596
|
+
"currencyCode",
|
|
597
|
+
selected.currencyCode ?? currencyCode ?? null
|
|
598
|
+
);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const fallbackTax = resolveTaxSelection(options?.fallbackTaxSource ?? null);
|
|
602
|
+
setFormValue("taxRate", fallbackTax.taxRate ?? null);
|
|
603
|
+
setFormValue("taxRateId", fallbackTax.taxRateId ?? null);
|
|
604
|
+
},
|
|
605
|
+
[currencyCode, findTaxRateIdByValue, resolveTaxSelection, resolveUnitPriceFactor]
|
|
606
|
+
);
|
|
349
607
|
const loadLineStatuses = React.useCallback(async () => {
|
|
350
608
|
setLineStatusLoading(true);
|
|
351
609
|
try {
|
|
352
610
|
const params = new URLSearchParams({ page: "1", pageSize: "100" });
|
|
353
|
-
const response = await apiCall(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
{ fallback: { items: [] } }
|
|
357
|
-
);
|
|
611
|
+
const response = await apiCall(`/api/sales/order-line-statuses?${params.toString()}`, void 0, {
|
|
612
|
+
fallback: { items: [] }
|
|
613
|
+
});
|
|
358
614
|
const items = Array.isArray(response.result?.items) ? response.result.items : [];
|
|
359
615
|
const mapped = items.map((entry) => {
|
|
360
616
|
const id = typeof entry.id === "string" ? entry.id : null;
|
|
@@ -379,18 +635,15 @@ function LineItemDialog({
|
|
|
379
635
|
async (query) => {
|
|
380
636
|
const options = lineStatuses.length && !query ? lineStatuses : await loadLineStatuses();
|
|
381
637
|
const term = query?.trim().toLowerCase() ?? "";
|
|
382
|
-
const currentMap = options.reduce(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
},
|
|
392
|
-
{}
|
|
393
|
-
);
|
|
638
|
+
const currentMap = options.reduce((acc, entry) => {
|
|
639
|
+
acc[entry.value] = {
|
|
640
|
+
value: entry.value,
|
|
641
|
+
label: entry.label,
|
|
642
|
+
color: entry.color,
|
|
643
|
+
icon: entry.icon ?? null
|
|
644
|
+
};
|
|
645
|
+
return acc;
|
|
646
|
+
}, {});
|
|
394
647
|
return options.filter(
|
|
395
648
|
(option) => !term.length || option.label.toLowerCase().includes(term) || option.value.toLowerCase().includes(term)
|
|
396
649
|
).map((option) => ({
|
|
@@ -411,44 +664,80 @@ function LineItemDialog({
|
|
|
411
664
|
}, [loadLineStatuses, loadTaxRates, open]);
|
|
412
665
|
const handleFormSubmit = React.useCallback(
|
|
413
666
|
async (values) => {
|
|
414
|
-
console.groupCollapsed("sales.line.submit.start");
|
|
415
|
-
console.log("raw values", values);
|
|
416
667
|
const resolvedDocumentId = typeof documentId === "string" && documentId.trim().length ? documentId : null;
|
|
417
668
|
const resolvedOrg = resolvedOrganizationId;
|
|
418
669
|
const resolvedTenant = resolvedTenantId;
|
|
419
670
|
if (!resolvedOrg || !resolvedTenant || !resolvedDocumentId) {
|
|
420
671
|
throw createCrudFormError(
|
|
421
|
-
t(
|
|
672
|
+
t(
|
|
673
|
+
"sales.documents.items.errorScope",
|
|
674
|
+
"Organization and tenant are required."
|
|
675
|
+
)
|
|
422
676
|
);
|
|
423
677
|
}
|
|
424
678
|
const lineMode2 = values.lineMode === "custom" ? "custom" : "catalog";
|
|
425
679
|
const isCustomLine = lineMode2 === "custom";
|
|
426
680
|
if (!isCustomLine && !values.productId) {
|
|
427
681
|
throw createCrudFormError(
|
|
428
|
-
t(
|
|
429
|
-
|
|
682
|
+
t(
|
|
683
|
+
"sales.documents.items.errorProductRequired",
|
|
684
|
+
"Select a product to continue."
|
|
685
|
+
),
|
|
686
|
+
{
|
|
687
|
+
productId: t(
|
|
688
|
+
"sales.documents.items.errorProductRequired",
|
|
689
|
+
"Select a product to continue."
|
|
690
|
+
)
|
|
691
|
+
}
|
|
430
692
|
);
|
|
431
693
|
}
|
|
432
694
|
if (!isCustomLine && !values.variantId) {
|
|
433
695
|
throw createCrudFormError(
|
|
434
|
-
t(
|
|
435
|
-
|
|
696
|
+
t(
|
|
697
|
+
"sales.documents.items.errorVariantRequired",
|
|
698
|
+
"Select a variant to continue."
|
|
699
|
+
),
|
|
700
|
+
{
|
|
701
|
+
variantId: t(
|
|
702
|
+
"sales.documents.items.errorVariantRequired",
|
|
703
|
+
"Select a variant to continue."
|
|
704
|
+
)
|
|
705
|
+
}
|
|
436
706
|
);
|
|
437
707
|
}
|
|
438
|
-
const qtyNumber = Number(values.quantity ??
|
|
439
|
-
console.log("quantity raw -> parsed", { raw: values.quantity, parsed: qtyNumber });
|
|
708
|
+
const qtyNumber = Number(values.quantity ?? 0);
|
|
440
709
|
if (!Number.isFinite(qtyNumber) || qtyNumber <= 0) {
|
|
441
710
|
throw createCrudFormError(
|
|
442
|
-
t(
|
|
443
|
-
|
|
711
|
+
t(
|
|
712
|
+
"sales.documents.items.errorQuantity",
|
|
713
|
+
"Quantity must be greater than 0."
|
|
714
|
+
),
|
|
715
|
+
{
|
|
716
|
+
quantity: t(
|
|
717
|
+
"sales.documents.items.errorQuantity",
|
|
718
|
+
"Quantity must be greater than 0."
|
|
719
|
+
)
|
|
720
|
+
}
|
|
444
721
|
);
|
|
445
722
|
}
|
|
446
|
-
const
|
|
447
|
-
|
|
723
|
+
const resolvedQuantityUnit = (() => {
|
|
724
|
+
const entered = normalizeUnitCode(values.quantityUnit);
|
|
725
|
+
if (isCustomLine) return entered;
|
|
726
|
+
return entered ?? normalizeUnitCode(productOption?.defaultSalesUnit) ?? normalizeUnitCode(productOption?.defaultUnit);
|
|
727
|
+
})();
|
|
728
|
+
const unitPriceNumber = Number(values.unitPrice ?? 0);
|
|
448
729
|
if (!Number.isFinite(unitPriceNumber) || unitPriceNumber <= 0) {
|
|
449
730
|
throw createCrudFormError(
|
|
450
|
-
t(
|
|
451
|
-
|
|
731
|
+
t(
|
|
732
|
+
"sales.documents.items.errorUnitPrice",
|
|
733
|
+
"Unit price must be greater than 0."
|
|
734
|
+
),
|
|
735
|
+
{
|
|
736
|
+
unitPrice: t(
|
|
737
|
+
"sales.documents.items.errorUnitPrice",
|
|
738
|
+
"Unit price must be greater than 0."
|
|
739
|
+
)
|
|
740
|
+
}
|
|
452
741
|
);
|
|
453
742
|
}
|
|
454
743
|
const selectedPrice = !isCustomLine && values.priceId ? priceOptions.find((price) => price.id === values.priceId) ?? null : null;
|
|
@@ -456,15 +745,28 @@ function LineItemDialog({
|
|
|
456
745
|
if (!resolvedCurrency) {
|
|
457
746
|
throw createCrudFormError(
|
|
458
747
|
t("sales.documents.items.errorCurrency", "Currency is required."),
|
|
459
|
-
{
|
|
748
|
+
{
|
|
749
|
+
priceId: t(
|
|
750
|
+
"sales.documents.items.errorCurrency",
|
|
751
|
+
"Currency is required."
|
|
752
|
+
)
|
|
753
|
+
}
|
|
460
754
|
);
|
|
461
755
|
}
|
|
462
756
|
const resolvedNameRaw = (values.name ?? "").toString().trim();
|
|
463
757
|
const resolvedName = isCustomLine ? resolvedNameRaw : resolvedNameRaw || variantOption?.title || productOption?.title || void 0;
|
|
464
758
|
if (isCustomLine && !resolvedName) {
|
|
465
759
|
throw createCrudFormError(
|
|
466
|
-
t(
|
|
467
|
-
|
|
760
|
+
t(
|
|
761
|
+
"sales.documents.items.errorNameRequired",
|
|
762
|
+
"Name is required for custom lines."
|
|
763
|
+
),
|
|
764
|
+
{
|
|
765
|
+
name: t(
|
|
766
|
+
"sales.documents.items.errorNameRequired",
|
|
767
|
+
"Name is required for custom lines."
|
|
768
|
+
)
|
|
769
|
+
}
|
|
468
770
|
);
|
|
469
771
|
}
|
|
470
772
|
const resolvedPriceMode = values.priceMode === "net" ? "net" : "gross";
|
|
@@ -494,7 +796,8 @@ function LineItemDialog({
|
|
|
494
796
|
variantThumbnail: variantOption.thumbnailUrl ?? productOption?.thumbnailUrl ?? null
|
|
495
797
|
} : {},
|
|
496
798
|
...isCustomLine ? { customLine: true } : {},
|
|
497
|
-
lineMode: lineMode2
|
|
799
|
+
lineMode: lineMode2,
|
|
800
|
+
...resolvedQuantityUnit ? { quantityUnit: resolvedQuantityUnit } : {}
|
|
498
801
|
};
|
|
499
802
|
const payload = {
|
|
500
803
|
[documentKey]: String(resolvedDocumentId),
|
|
@@ -503,6 +806,7 @@ function LineItemDialog({
|
|
|
503
806
|
productId: isCustomLine ? void 0 : values.productId ? String(values.productId) : void 0,
|
|
504
807
|
productVariantId: isCustomLine ? void 0 : values.variantId ? String(values.variantId) : void 0,
|
|
505
808
|
quantity: qtyNumber,
|
|
809
|
+
quantityUnit: resolvedQuantityUnit ?? void 0,
|
|
506
810
|
currencyCode: String(resolvedCurrency),
|
|
507
811
|
priceId: !isCustomLine && values.priceId ? String(values.priceId) : void 0,
|
|
508
812
|
priceMode: resolvedPriceMode,
|
|
@@ -523,18 +827,16 @@ function LineItemDialog({
|
|
|
523
827
|
payload.customFields = normalizeCustomFieldValues(customFields);
|
|
524
828
|
}
|
|
525
829
|
if (resolvedName) payload.name = resolvedName;
|
|
526
|
-
console.debug("resolved scope", { resolvedDocumentId, resolvedOrg, resolvedTenant, resolvedCurrency });
|
|
527
|
-
console.debug("parsed numbers", { qtyNumber, unitPriceNumber });
|
|
528
|
-
console.log("sales.line.submit.payload", payload);
|
|
529
|
-
console.log("sales.line.submit.payload.json", JSON.stringify(payload));
|
|
530
|
-
console.groupEnd();
|
|
531
830
|
try {
|
|
532
831
|
const action = editingId ? updateCrud : createCrud;
|
|
533
832
|
const result = await action(
|
|
534
833
|
resourcePath,
|
|
535
834
|
editingId ? { id: editingId, ...payload } : payload,
|
|
536
835
|
{
|
|
537
|
-
errorMessage: t(
|
|
836
|
+
errorMessage: t(
|
|
837
|
+
"sales.documents.items.errorSave",
|
|
838
|
+
"Failed to save line."
|
|
839
|
+
)
|
|
538
840
|
}
|
|
539
841
|
);
|
|
540
842
|
if (result.ok) {
|
|
@@ -542,7 +844,6 @@ function LineItemDialog({
|
|
|
542
844
|
closeDialog();
|
|
543
845
|
}
|
|
544
846
|
} catch (err) {
|
|
545
|
-
console.error("sales.line.submit.error", err);
|
|
546
847
|
throw err;
|
|
547
848
|
}
|
|
548
849
|
},
|
|
@@ -580,10 +881,12 @@ function LineItemDialog({
|
|
|
580
881
|
setProductOption(null);
|
|
581
882
|
setVariantOption(null);
|
|
582
883
|
setPriceOptions([]);
|
|
884
|
+
setUnitOptions([]);
|
|
583
885
|
setFormValue?.("productId", null);
|
|
584
886
|
setFormValue?.("variantId", null);
|
|
585
887
|
setFormValue?.("priceId", null);
|
|
586
888
|
setFormValue?.("catalogSnapshot", null);
|
|
889
|
+
setFormValue?.("quantityUnit", null);
|
|
587
890
|
} else {
|
|
588
891
|
setFormValue?.("unitPrice", "");
|
|
589
892
|
setFormValue?.("priceMode", "gross");
|
|
@@ -626,7 +929,12 @@ function LineItemDialog({
|
|
|
626
929
|
type: "custom",
|
|
627
930
|
required: true,
|
|
628
931
|
layout: "half",
|
|
629
|
-
component: ({
|
|
932
|
+
component: ({
|
|
933
|
+
value,
|
|
934
|
+
setValue,
|
|
935
|
+
setFormValue,
|
|
936
|
+
values
|
|
937
|
+
}) => /* @__PURE__ */ jsx(
|
|
630
938
|
LookupSelect,
|
|
631
939
|
{
|
|
632
940
|
value: typeof value === "string" ? value : null,
|
|
@@ -635,11 +943,22 @@ function LineItemDialog({
|
|
|
635
943
|
setProductOption(selectedOption);
|
|
636
944
|
setVariantOption(null);
|
|
637
945
|
setPriceOptions([]);
|
|
946
|
+
setUnitOptions([]);
|
|
638
947
|
setValue(next ?? null);
|
|
639
948
|
setFormValue?.("variantId", null);
|
|
640
949
|
setFormValue?.("priceId", null);
|
|
641
950
|
setFormValue?.("unitPrice", "");
|
|
642
951
|
setFormValue?.("priceMode", "gross");
|
|
952
|
+
const defaultQuantityUnit = selectedOption?.defaultSalesUnit ?? selectedOption?.defaultUnit ?? null;
|
|
953
|
+
setFormValue?.("quantityUnit", defaultQuantityUnit);
|
|
954
|
+
if (typeof selectedOption?.defaultSalesUnitQuantity === "number" && Number.isFinite(
|
|
955
|
+
selectedOption.defaultSalesUnitQuantity
|
|
956
|
+
) && selectedOption.defaultSalesUnitQuantity > 0) {
|
|
957
|
+
setFormValue?.(
|
|
958
|
+
"quantity",
|
|
959
|
+
String(selectedOption.defaultSalesUnitQuantity)
|
|
960
|
+
);
|
|
961
|
+
}
|
|
643
962
|
const taxSelection = selectedOption ? resolveTaxSelection(selectedOption) : { taxRate: null, taxRateId: null };
|
|
644
963
|
setFormValue?.("taxRate", taxSelection.taxRate ?? null);
|
|
645
964
|
setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
|
|
@@ -654,12 +973,20 @@ function LineItemDialog({
|
|
|
654
973
|
id: next,
|
|
655
974
|
title: selectedOption?.title ?? null,
|
|
656
975
|
sku: selectedOption?.sku ?? null,
|
|
657
|
-
thumbnailUrl: selectedOption?.thumbnailUrl ?? null
|
|
976
|
+
thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
|
|
977
|
+
defaultUnit: selectedOption?.defaultUnit ?? null,
|
|
978
|
+
defaultSalesUnit: selectedOption?.defaultSalesUnit ?? null
|
|
658
979
|
}
|
|
659
980
|
} : null
|
|
660
981
|
);
|
|
661
982
|
if (next) {
|
|
662
|
-
void
|
|
983
|
+
void loadProductUnits(next, selectedOption);
|
|
984
|
+
void loadPrices(
|
|
985
|
+
next,
|
|
986
|
+
null,
|
|
987
|
+
typeof values?.quantity === "string" ? values.quantity : 1,
|
|
988
|
+
defaultQuantityUnit
|
|
989
|
+
);
|
|
663
990
|
}
|
|
664
991
|
},
|
|
665
992
|
fetchItems: loadProductOptions,
|
|
@@ -668,20 +995,42 @@ function LineItemDialog({
|
|
|
668
995
|
id: productOption.id,
|
|
669
996
|
title: productOption.title || productOption.id,
|
|
670
997
|
subtitle: productOption.sku ?? void 0,
|
|
671
|
-
icon: productOption.thumbnailUrl ? /* @__PURE__ */ jsx(
|
|
998
|
+
icon: productOption.thumbnailUrl ? /* @__PURE__ */ jsx(
|
|
999
|
+
"img",
|
|
1000
|
+
{
|
|
1001
|
+
src: productOption.thumbnailUrl,
|
|
1002
|
+
alt: productOption.title ?? productOption.id,
|
|
1003
|
+
className: "h-8 w-8 rounded object-cover"
|
|
1004
|
+
}
|
|
1005
|
+
) : buildPlaceholder(
|
|
1006
|
+
productOption.title || productOption.id
|
|
1007
|
+
)
|
|
672
1008
|
}
|
|
673
1009
|
] : void 0,
|
|
674
1010
|
minQuery: 1,
|
|
675
|
-
searchPlaceholder: t(
|
|
1011
|
+
searchPlaceholder: t(
|
|
1012
|
+
"sales.documents.items.productSearch",
|
|
1013
|
+
"Search product"
|
|
1014
|
+
),
|
|
676
1015
|
selectLabel: t("ui.lookupSelect.select", "Select"),
|
|
677
1016
|
selectedLabel: t("ui.lookupSelect.selected", "Selected"),
|
|
678
|
-
clearLabel: t(
|
|
1017
|
+
clearLabel: t(
|
|
1018
|
+
"ui.lookupSelect.clearSelection",
|
|
1019
|
+
"Clear selection"
|
|
1020
|
+
),
|
|
679
1021
|
emptyLabel: t("ui.lookupSelect.noResults", "No results"),
|
|
680
1022
|
loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
|
|
681
|
-
startTypingLabel: t(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1023
|
+
startTypingLabel: t(
|
|
1024
|
+
"ui.lookupSelect.startTyping",
|
|
1025
|
+
"Start typing to search."
|
|
1026
|
+
),
|
|
1027
|
+
selectedHintLabel: (id) => t(
|
|
1028
|
+
"sales.documents.items.selectedProduct",
|
|
1029
|
+
"Selected {{id}}",
|
|
1030
|
+
{
|
|
1031
|
+
id: productOption?.title ?? id
|
|
1032
|
+
}
|
|
1033
|
+
)
|
|
685
1034
|
}
|
|
686
1035
|
)
|
|
687
1036
|
},
|
|
@@ -691,7 +1040,12 @@ function LineItemDialog({
|
|
|
691
1040
|
type: "custom",
|
|
692
1041
|
required: true,
|
|
693
1042
|
layout: "half",
|
|
694
|
-
component: ({
|
|
1043
|
+
component: ({
|
|
1044
|
+
value,
|
|
1045
|
+
setValue,
|
|
1046
|
+
setFormValue,
|
|
1047
|
+
values
|
|
1048
|
+
}) => {
|
|
695
1049
|
const productId = typeof values?.productId === "string" ? values.productId : null;
|
|
696
1050
|
return /* @__PURE__ */ jsx(
|
|
697
1051
|
LookupSelect,
|
|
@@ -703,13 +1057,19 @@ function LineItemDialog({
|
|
|
703
1057
|
setValue(next ?? null);
|
|
704
1058
|
const existingName = typeof values?.name === "string" ? values.name : "";
|
|
705
1059
|
if (!existingName.trim()) {
|
|
706
|
-
setFormValue?.(
|
|
1060
|
+
setFormValue?.(
|
|
1061
|
+
"name",
|
|
1062
|
+
selectedOption?.title ?? productOption?.title ?? existingName
|
|
1063
|
+
);
|
|
707
1064
|
}
|
|
708
1065
|
const taxSource = hasTaxMetadata(selectedOption) ? selectedOption : hasTaxMetadata(productOption) ? productOption : null;
|
|
709
1066
|
if (taxSource) {
|
|
710
1067
|
const taxSelection = resolveTaxSelection(taxSource);
|
|
711
1068
|
setFormValue?.("taxRate", taxSelection.taxRate ?? null);
|
|
712
|
-
setFormValue?.(
|
|
1069
|
+
setFormValue?.(
|
|
1070
|
+
"taxRateId",
|
|
1071
|
+
taxSelection.taxRateId ?? null
|
|
1072
|
+
);
|
|
713
1073
|
}
|
|
714
1074
|
const prevSnapshot = typeof values?.catalogSnapshot === "object" && values.catalogSnapshot ? values.catalogSnapshot : null;
|
|
715
1075
|
if (next) {
|
|
@@ -724,29 +1084,54 @@ function LineItemDialog({
|
|
|
724
1084
|
});
|
|
725
1085
|
} else if (prevSnapshot) {
|
|
726
1086
|
const snapshot = { ...prevSnapshot };
|
|
727
|
-
if ("variant" in snapshot)
|
|
728
|
-
|
|
1087
|
+
if ("variant" in snapshot)
|
|
1088
|
+
delete snapshot.variant;
|
|
1089
|
+
setFormValue?.(
|
|
1090
|
+
"catalogSnapshot",
|
|
1091
|
+
Object.keys(snapshot).length ? snapshot : null
|
|
1092
|
+
);
|
|
729
1093
|
} else {
|
|
730
1094
|
setFormValue?.("catalogSnapshot", null);
|
|
731
1095
|
}
|
|
732
1096
|
if (productId) {
|
|
733
|
-
|
|
1097
|
+
const currentQuantity = typeof values?.quantity === "string" ? values.quantity : 1;
|
|
1098
|
+
const currentQuantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
|
|
1099
|
+
void loadPrices(
|
|
1100
|
+
productId,
|
|
1101
|
+
next,
|
|
1102
|
+
currentQuantity,
|
|
1103
|
+
currentQuantityUnit
|
|
1104
|
+
);
|
|
734
1105
|
}
|
|
735
1106
|
},
|
|
736
1107
|
fetchItems: async (query) => {
|
|
737
1108
|
if (!productId) return [];
|
|
738
1109
|
const productThumb = productId ? productOptionsRef.current.get(productId)?.thumbnailUrl : null;
|
|
739
|
-
const options = await loadVariantOptions(
|
|
1110
|
+
const options = await loadVariantOptions(
|
|
1111
|
+
productId,
|
|
1112
|
+
productThumb
|
|
1113
|
+
);
|
|
740
1114
|
const needle = query?.trim().toLowerCase() ?? "";
|
|
741
|
-
return needle.length ? options.filter(
|
|
1115
|
+
return needle.length ? options.filter(
|
|
1116
|
+
(option) => option.title.toLowerCase().includes(needle)
|
|
1117
|
+
) : options;
|
|
742
1118
|
},
|
|
743
|
-
searchPlaceholder: t(
|
|
1119
|
+
searchPlaceholder: t(
|
|
1120
|
+
"sales.documents.items.variantSearch",
|
|
1121
|
+
"Search variant"
|
|
1122
|
+
),
|
|
744
1123
|
selectLabel: t("ui.lookupSelect.select", "Select"),
|
|
745
1124
|
selectedLabel: t("ui.lookupSelect.selected", "Selected"),
|
|
746
|
-
clearLabel: t(
|
|
1125
|
+
clearLabel: t(
|
|
1126
|
+
"ui.lookupSelect.clearSelection",
|
|
1127
|
+
"Clear selection"
|
|
1128
|
+
),
|
|
747
1129
|
emptyLabel: t("ui.lookupSelect.noResults", "No results"),
|
|
748
1130
|
loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
|
|
749
|
-
startTypingLabel: t(
|
|
1131
|
+
startTypingLabel: t(
|
|
1132
|
+
"ui.lookupSelect.startTyping",
|
|
1133
|
+
"Start typing to search."
|
|
1134
|
+
),
|
|
750
1135
|
minQuery: 0,
|
|
751
1136
|
options: variantOption ? [
|
|
752
1137
|
{
|
|
@@ -760,12 +1145,18 @@ function LineItemDialog({
|
|
|
760
1145
|
alt: variantOption.title ?? variantOption.id,
|
|
761
1146
|
className: "h-8 w-8 rounded object-cover"
|
|
762
1147
|
}
|
|
763
|
-
) : buildPlaceholder(
|
|
1148
|
+
) : buildPlaceholder(
|
|
1149
|
+
variantOption.title || variantOption.id
|
|
1150
|
+
)
|
|
764
1151
|
}
|
|
765
1152
|
] : void 0,
|
|
766
|
-
selectedHintLabel: (id) => t(
|
|
767
|
-
|
|
768
|
-
|
|
1153
|
+
selectedHintLabel: (id) => t(
|
|
1154
|
+
"sales.documents.items.selectedVariant",
|
|
1155
|
+
"Selected {{id}}",
|
|
1156
|
+
{
|
|
1157
|
+
id: variantOption?.title ?? id
|
|
1158
|
+
}
|
|
1159
|
+
),
|
|
769
1160
|
disabled: !productId
|
|
770
1161
|
},
|
|
771
1162
|
productId ?? "no-product"
|
|
@@ -777,7 +1168,12 @@ function LineItemDialog({
|
|
|
777
1168
|
label: t("sales.documents.items.price", "Price"),
|
|
778
1169
|
type: "custom",
|
|
779
1170
|
layout: "half",
|
|
780
|
-
component: ({
|
|
1171
|
+
component: ({
|
|
1172
|
+
value,
|
|
1173
|
+
setValue,
|
|
1174
|
+
setFormValue,
|
|
1175
|
+
values
|
|
1176
|
+
}) => {
|
|
781
1177
|
const productId = typeof values?.productId === "string" ? values.productId : null;
|
|
782
1178
|
const variantId = typeof values?.variantId === "string" ? values.variantId : null;
|
|
783
1179
|
return /* @__PURE__ */ jsx(
|
|
@@ -787,26 +1183,22 @@ function LineItemDialog({
|
|
|
787
1183
|
onChange: (next) => {
|
|
788
1184
|
setValue(next ?? null);
|
|
789
1185
|
const selected = next ? priceOptions.find((entry) => entry.id === next) ?? null : null;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
setFormValue?.(
|
|
799
|
-
"currencyCode",
|
|
800
|
-
selected.currencyCode ?? values?.currencyCode ?? currencyCode ?? null
|
|
801
|
-
);
|
|
802
|
-
} else {
|
|
803
|
-
const fallbackTax = resolveTaxSelection(variantOption ?? productOption ?? null);
|
|
804
|
-
setFormValue?.("taxRate", fallbackTax.taxRate ?? null);
|
|
805
|
-
setFormValue?.("taxRateId", fallbackTax.taxRateId ?? null);
|
|
806
|
-
}
|
|
1186
|
+
applyPriceSelection(
|
|
1187
|
+
selected,
|
|
1188
|
+
setFormValue,
|
|
1189
|
+
{
|
|
1190
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
1191
|
+
quantityUnit: typeof values?.quantityUnit === "string" ? values.quantityUnit : null
|
|
1192
|
+
}
|
|
1193
|
+
);
|
|
807
1194
|
},
|
|
808
1195
|
fetchItems: async (query) => {
|
|
809
|
-
const prices = await loadPrices(
|
|
1196
|
+
const prices = await loadPrices(
|
|
1197
|
+
productId,
|
|
1198
|
+
variantId,
|
|
1199
|
+
typeof values?.quantity === "string" ? values.quantity : 1,
|
|
1200
|
+
typeof values?.quantityUnit === "string" ? values.quantityUnit : null
|
|
1201
|
+
);
|
|
810
1202
|
const needle = query?.trim().toLowerCase() ?? "";
|
|
811
1203
|
return prices.filter((price) => {
|
|
812
1204
|
if (!needle.length) return true;
|
|
@@ -829,13 +1221,22 @@ function LineItemDialog({
|
|
|
829
1221
|
},
|
|
830
1222
|
minQuery: 0,
|
|
831
1223
|
loading: priceLoading,
|
|
832
|
-
searchPlaceholder: t(
|
|
1224
|
+
searchPlaceholder: t(
|
|
1225
|
+
"sales.documents.items.priceSearch",
|
|
1226
|
+
"Select price"
|
|
1227
|
+
),
|
|
833
1228
|
selectLabel: t("ui.lookupSelect.select", "Select"),
|
|
834
1229
|
selectedLabel: t("ui.lookupSelect.selected", "Selected"),
|
|
835
|
-
clearLabel: t(
|
|
1230
|
+
clearLabel: t(
|
|
1231
|
+
"ui.lookupSelect.clearSelection",
|
|
1232
|
+
"Clear selection"
|
|
1233
|
+
),
|
|
836
1234
|
emptyLabel: t("ui.lookupSelect.noResults", "No results"),
|
|
837
1235
|
loadingLabel: t("ui.lookupSelect.searching", "Searching\u2026"),
|
|
838
|
-
startTypingLabel: t(
|
|
1236
|
+
startTypingLabel: t(
|
|
1237
|
+
"ui.lookupSelect.startTyping",
|
|
1238
|
+
"Start typing to search."
|
|
1239
|
+
),
|
|
839
1240
|
disabled: !productId
|
|
840
1241
|
},
|
|
841
1242
|
productId ? `${productId}-${variantId ?? "no-variant"}` : "price"
|
|
@@ -848,32 +1249,72 @@ function LineItemDialog({
|
|
|
848
1249
|
label: t("sales.documents.items.unitPrice", "Unit price"),
|
|
849
1250
|
type: "custom",
|
|
850
1251
|
layout: "half",
|
|
851
|
-
component: ({
|
|
1252
|
+
component: ({
|
|
1253
|
+
value,
|
|
1254
|
+
setValue,
|
|
1255
|
+
setFormValue,
|
|
1256
|
+
values
|
|
1257
|
+
}) => {
|
|
852
1258
|
const mode = values?.priceMode === "net" ? "net" : "gross";
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
|
|
1259
|
+
const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
|
|
1260
|
+
const selectedPrice = selectedPriceId ? priceOptions.find((entry) => entry.id === selectedPriceId) ?? null : null;
|
|
1261
|
+
const selectedCurrency = selectedPrice?.currencyCode ?? (typeof values?.currencyCode === "string" ? values.currencyCode : null);
|
|
1262
|
+
const quantityUnitCode = normalizeUnitCode(values?.quantityUnit);
|
|
1263
|
+
const baseUnitCode = unitOptions.find((option) => option.isBase)?.code ?? null;
|
|
1264
|
+
const selectedUnitOption = quantityUnitCode ? unitOptions.find((option) => option.code === quantityUnitCode) ?? null : null;
|
|
1265
|
+
const unitFactor = (() => {
|
|
1266
|
+
if (!quantityUnitCode) return null;
|
|
1267
|
+
if (baseUnitCode && quantityUnitCode === baseUnitCode) return 1;
|
|
1268
|
+
const value2 = normalizeNumber(selectedUnitOption?.toBaseFactor, Number.NaN);
|
|
1269
|
+
return Number.isFinite(value2) && value2 > 0 ? value2 : null;
|
|
1270
|
+
})();
|
|
1271
|
+
const selectedBaseAmount = selectedPrice ? mode === "net" ? selectedPrice.amountNet ?? selectedPrice.amountGross ?? null : selectedPrice.amountGross ?? selectedPrice.amountNet ?? null : null;
|
|
1272
|
+
const convertedAmount = selectedBaseAmount !== null && unitFactor !== null && Number.isFinite(selectedBaseAmount * unitFactor) ? selectedBaseAmount * unitFactor : null;
|
|
1273
|
+
const isCatalogLine = lineMode !== "custom";
|
|
1274
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1275
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
1276
|
+
/* @__PURE__ */ jsx(
|
|
1277
|
+
Input,
|
|
1278
|
+
{
|
|
1279
|
+
value: typeof value === "string" ? value : value == null ? "" : String(value),
|
|
1280
|
+
onChange: (event) => setValue(event.target.value),
|
|
1281
|
+
placeholder: "0.00"
|
|
1282
|
+
}
|
|
1283
|
+
),
|
|
1284
|
+
/* @__PURE__ */ jsxs(
|
|
1285
|
+
"select",
|
|
1286
|
+
{
|
|
1287
|
+
className: "w-32 rounded border px-2 text-sm",
|
|
1288
|
+
value: mode,
|
|
1289
|
+
onChange: (event) => {
|
|
1290
|
+
const nextMode = event.target.value === "net" ? "net" : "gross";
|
|
1291
|
+
setFormValue?.("priceMode", nextMode);
|
|
1292
|
+
},
|
|
1293
|
+
children: [
|
|
1294
|
+
/* @__PURE__ */ jsx("option", { value: "gross", children: t("sales.documents.items.priceGross", "Gross") }),
|
|
1295
|
+
/* @__PURE__ */ jsx("option", { value: "net", children: t("sales.documents.items.priceNet", "Net") })
|
|
1296
|
+
]
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
] }),
|
|
1300
|
+
isCatalogLine && selectedPrice && quantityUnitCode && baseUnitCode ? unitFactor !== null && convertedAmount !== null ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1301
|
+
"sales.documents.items.priceBasisTemplate",
|
|
1302
|
+
"Catalog price basis: {{baseAmount}} / {{baseUnit}}. Converted for {{unit}}: {{baseAmount}} \xD7 {{factor}} = {{convertedAmount}}.",
|
|
864
1303
|
{
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
},
|
|
871
|
-
children: [
|
|
872
|
-
/* @__PURE__ */ jsx("option", { value: "gross", children: t("sales.documents.items.priceGross", "Gross") }),
|
|
873
|
-
/* @__PURE__ */ jsx("option", { value: "net", children: t("sales.documents.items.priceNet", "Net") })
|
|
874
|
-
]
|
|
1304
|
+
baseAmount: formatMoney(selectedBaseAmount, selectedCurrency),
|
|
1305
|
+
baseUnit: baseUnitCode,
|
|
1306
|
+
unit: quantityUnitCode,
|
|
1307
|
+
factor: unitFactor,
|
|
1308
|
+
convertedAmount: formatMoney(convertedAmount, selectedCurrency)
|
|
875
1309
|
}
|
|
876
|
-
)
|
|
1310
|
+
) }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1311
|
+
"sales.documents.items.priceBasisMissingConversion",
|
|
1312
|
+
"Catalog price basis is base unit, but conversion for selected unit is missing."
|
|
1313
|
+
) }) : null,
|
|
1314
|
+
isCatalogLine && !selectedPrice ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1315
|
+
"sales.documents.items.priceBasisManual",
|
|
1316
|
+
"Manual unit price mode. Price will not auto-convert until you select a catalog price."
|
|
1317
|
+
) }) : null
|
|
877
1318
|
] });
|
|
878
1319
|
}
|
|
879
1320
|
},
|
|
@@ -882,7 +1323,12 @@ function LineItemDialog({
|
|
|
882
1323
|
label: t("sales.documents.items.taxRate", "Tax class"),
|
|
883
1324
|
type: "custom",
|
|
884
1325
|
layout: "half",
|
|
885
|
-
component: ({
|
|
1326
|
+
component: ({
|
|
1327
|
+
value,
|
|
1328
|
+
setValue,
|
|
1329
|
+
setFormValue,
|
|
1330
|
+
values
|
|
1331
|
+
}) => {
|
|
886
1332
|
const resolvedValue = typeof value === "string" && value.trim().length ? value : findTaxRateIdByValue(values?.taxRate);
|
|
887
1333
|
const handleChange = (event) => {
|
|
888
1334
|
const nextId = event.target.value || null;
|
|
@@ -900,7 +1346,13 @@ function LineItemDialog({
|
|
|
900
1346
|
onChange: handleChange,
|
|
901
1347
|
disabled: !taxRates.length,
|
|
902
1348
|
children: [
|
|
903
|
-
/* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
|
|
1349
|
+
/* @__PURE__ */ jsx("option", { value: "", children: taxRates.length ? t(
|
|
1350
|
+
"sales.documents.items.taxRate.none",
|
|
1351
|
+
"No tax class selected"
|
|
1352
|
+
) : t(
|
|
1353
|
+
"sales.documents.items.taxRate.empty",
|
|
1354
|
+
"No tax classes available"
|
|
1355
|
+
) }),
|
|
904
1356
|
taxRates.map((rate) => /* @__PURE__ */ jsxs("option", { value: rate.id, children: [
|
|
905
1357
|
rate.name,
|
|
906
1358
|
rate.code ? ` \u2022 ${rate.code.toUpperCase()}` : "",
|
|
@@ -917,30 +1369,187 @@ function LineItemDialog({
|
|
|
917
1369
|
size: "icon",
|
|
918
1370
|
onClick: () => {
|
|
919
1371
|
if (typeof window !== "undefined") {
|
|
920
|
-
window.open(
|
|
1372
|
+
window.open(
|
|
1373
|
+
"/backend/config/sales?section=tax-rates",
|
|
1374
|
+
"_blank",
|
|
1375
|
+
"noopener,noreferrer"
|
|
1376
|
+
);
|
|
921
1377
|
}
|
|
922
1378
|
},
|
|
923
|
-
title: t(
|
|
1379
|
+
title: t(
|
|
1380
|
+
"catalog.products.create.taxRates.manage",
|
|
1381
|
+
"Manage tax classes"
|
|
1382
|
+
),
|
|
924
1383
|
children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
|
|
925
1384
|
}
|
|
926
1385
|
)
|
|
927
1386
|
] });
|
|
928
1387
|
}
|
|
929
1388
|
},
|
|
1389
|
+
{
|
|
1390
|
+
id: "quantityUnit",
|
|
1391
|
+
label: t("sales.documents.items.quantityUnit", "Unit"),
|
|
1392
|
+
type: "custom",
|
|
1393
|
+
layout: "half",
|
|
1394
|
+
component: ({ value, setValue, setFormValue, values }) => {
|
|
1395
|
+
const productId = typeof values?.productId === "string" ? values.productId : null;
|
|
1396
|
+
const variantId = typeof values?.variantId === "string" ? values.variantId : null;
|
|
1397
|
+
if (isCustomLine) {
|
|
1398
|
+
return /* @__PURE__ */ jsx(
|
|
1399
|
+
Input,
|
|
1400
|
+
{
|
|
1401
|
+
value: typeof value === "string" ? value : "",
|
|
1402
|
+
onChange: (event) => setValue(event.target.value || null),
|
|
1403
|
+
placeholder: t(
|
|
1404
|
+
"sales.documents.items.quantityUnitPlaceholder",
|
|
1405
|
+
"e.g. pc"
|
|
1406
|
+
)
|
|
1407
|
+
}
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return /* @__PURE__ */ jsxs(
|
|
1411
|
+
"select",
|
|
1412
|
+
{
|
|
1413
|
+
className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1414
|
+
value: typeof value === "string" ? value : "",
|
|
1415
|
+
onChange: (event) => {
|
|
1416
|
+
const nextValue = event.target.value || null;
|
|
1417
|
+
const nextUnitPrice = convertUnitPriceForUnitChange(
|
|
1418
|
+
values?.unitPrice,
|
|
1419
|
+
typeof value === "string" ? value : null,
|
|
1420
|
+
nextValue
|
|
1421
|
+
);
|
|
1422
|
+
setValue(nextValue);
|
|
1423
|
+
if (nextUnitPrice) {
|
|
1424
|
+
setFormValue?.("unitPrice", nextUnitPrice);
|
|
1425
|
+
}
|
|
1426
|
+
if (productId) {
|
|
1427
|
+
const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
|
|
1428
|
+
const selectedPriceKindId = selectedPriceId ? priceOptions.find(
|
|
1429
|
+
(entry) => entry.id === selectedPriceId
|
|
1430
|
+
)?.priceKindId ?? null : null;
|
|
1431
|
+
void loadPrices(
|
|
1432
|
+
productId,
|
|
1433
|
+
variantId,
|
|
1434
|
+
typeof values?.quantity === "string" ? values.quantity : 1,
|
|
1435
|
+
nextValue
|
|
1436
|
+
).then((prices) => {
|
|
1437
|
+
if (!selectedPriceId) return;
|
|
1438
|
+
const nextSelected = selectPriceAfterRefresh(
|
|
1439
|
+
prices,
|
|
1440
|
+
selectedPriceId,
|
|
1441
|
+
selectedPriceKindId
|
|
1442
|
+
);
|
|
1443
|
+
if (!nextSelected) {
|
|
1444
|
+
setFormValue?.("priceId", null);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
applyPriceSelection(
|
|
1448
|
+
nextSelected,
|
|
1449
|
+
setFormValue,
|
|
1450
|
+
{
|
|
1451
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
1452
|
+
quantityUnit: nextValue
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
disabled: !productId,
|
|
1459
|
+
children: [
|
|
1460
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t("sales.documents.items.quantityUnitSelect", "Select unit") }),
|
|
1461
|
+
unitOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.code, children: option.isBase ? `${option.code} (${t("sales.documents.items.baseUnitTag", "base")})` : option.code }, option.code))
|
|
1462
|
+
]
|
|
1463
|
+
}
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
930
1467
|
{
|
|
931
1468
|
id: "quantity",
|
|
932
1469
|
label: t("sales.documents.items.quantity", "Quantity"),
|
|
933
1470
|
type: "custom",
|
|
934
1471
|
layout: "half",
|
|
935
|
-
component: ({ value, setValue }) => /* @__PURE__ */ jsx(
|
|
1472
|
+
component: ({ value, setValue, setFormValue, values }) => /* @__PURE__ */ jsx(
|
|
936
1473
|
Input,
|
|
937
1474
|
{
|
|
938
1475
|
value: typeof value === "string" ? value : value == null ? "" : String(value),
|
|
939
|
-
onChange: (event) =>
|
|
1476
|
+
onChange: (event) => {
|
|
1477
|
+
const nextQuantity = event.target.value;
|
|
1478
|
+
setValue(nextQuantity);
|
|
1479
|
+
const productId = typeof values?.productId === "string" ? values.productId : null;
|
|
1480
|
+
const variantId = typeof values?.variantId === "string" ? values.variantId : null;
|
|
1481
|
+
const quantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
|
|
1482
|
+
if (productId) {
|
|
1483
|
+
const selectedPriceId = typeof values?.priceId === "string" ? values.priceId : null;
|
|
1484
|
+
const selectedPriceKindId = selectedPriceId ? priceOptions.find((entry) => entry.id === selectedPriceId)?.priceKindId ?? null : null;
|
|
1485
|
+
void loadPrices(
|
|
1486
|
+
productId,
|
|
1487
|
+
variantId,
|
|
1488
|
+
nextQuantity,
|
|
1489
|
+
quantityUnit
|
|
1490
|
+
).then((prices) => {
|
|
1491
|
+
if (!selectedPriceId) return;
|
|
1492
|
+
const nextSelected = selectPriceAfterRefresh(
|
|
1493
|
+
prices,
|
|
1494
|
+
selectedPriceId,
|
|
1495
|
+
selectedPriceKindId
|
|
1496
|
+
);
|
|
1497
|
+
if (!nextSelected) {
|
|
1498
|
+
setFormValue?.("priceId", null);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
applyPriceSelection(
|
|
1502
|
+
nextSelected,
|
|
1503
|
+
setFormValue,
|
|
1504
|
+
{
|
|
1505
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
1506
|
+
quantityUnit
|
|
1507
|
+
}
|
|
1508
|
+
);
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
940
1512
|
placeholder: "1"
|
|
941
1513
|
}
|
|
942
1514
|
)
|
|
943
1515
|
},
|
|
1516
|
+
{
|
|
1517
|
+
id: "uomPreview",
|
|
1518
|
+
label: t("sales.documents.items.uomPreview", "Normalized quantity"),
|
|
1519
|
+
type: "custom",
|
|
1520
|
+
layout: "full",
|
|
1521
|
+
component: ({ values }) => {
|
|
1522
|
+
if (isCustomLine) return null;
|
|
1523
|
+
const quantity = normalizeNumber(values?.quantity, Number.NaN);
|
|
1524
|
+
const enteredUnit = normalizeUnitCode(values?.quantityUnit);
|
|
1525
|
+
if (!Number.isFinite(quantity) || quantity <= 0 || !enteredUnit) {
|
|
1526
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1527
|
+
"sales.documents.items.uomPreviewEmpty",
|
|
1528
|
+
"Select unit and quantity to preview normalization."
|
|
1529
|
+
) });
|
|
1530
|
+
}
|
|
1531
|
+
const selectedOption = unitOptions.find((option) => option.code === enteredUnit) ?? null;
|
|
1532
|
+
const baseOption = unitOptions.find((option) => option.isBase) ?? null;
|
|
1533
|
+
const factor = selectedOption?.toBaseFactor;
|
|
1534
|
+
if (!Number.isFinite(factor) || !factor || !baseOption) {
|
|
1535
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1536
|
+
"sales.documents.items.uomPreviewUnavailable",
|
|
1537
|
+
"Missing conversion to base unit."
|
|
1538
|
+
) });
|
|
1539
|
+
}
|
|
1540
|
+
const normalized = normalizeQuantityPreview(quantity * factor);
|
|
1541
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
1542
|
+
"sales.documents.items.uomPreviewTemplate",
|
|
1543
|
+
"{{quantity}} {{unit}} \u2192 {{normalized}} {{baseUnit}}",
|
|
1544
|
+
{
|
|
1545
|
+
quantity,
|
|
1546
|
+
unit: enteredUnit,
|
|
1547
|
+
normalized,
|
|
1548
|
+
baseUnit: baseOption.code
|
|
1549
|
+
}
|
|
1550
|
+
) });
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
944
1553
|
{
|
|
945
1554
|
id: "statusEntryId",
|
|
946
1555
|
label: t("sales.documents.items.status", "Status"),
|
|
@@ -951,14 +1560,23 @@ function LineItemDialog({
|
|
|
951
1560
|
{
|
|
952
1561
|
value: typeof value === "string" ? value : null,
|
|
953
1562
|
onChange: (next) => setValue(next ?? null),
|
|
954
|
-
placeholder: t(
|
|
1563
|
+
placeholder: t(
|
|
1564
|
+
"sales.documents.items.statusPlaceholder",
|
|
1565
|
+
"Select status"
|
|
1566
|
+
),
|
|
955
1567
|
emptyLabel: t("sales.documents.items.statusEmpty", "No status"),
|
|
956
1568
|
fetchItems: fetchLineStatusItems,
|
|
957
|
-
loadingLabel: t(
|
|
1569
|
+
loadingLabel: t(
|
|
1570
|
+
"sales.documents.items.statusLoading",
|
|
1571
|
+
"Loading statuses\u2026"
|
|
1572
|
+
),
|
|
958
1573
|
selectLabel: t("ui.lookupSelect.select", "Select"),
|
|
959
1574
|
selectedLabel: t("ui.lookupSelect.selected", "Selected"),
|
|
960
1575
|
clearLabel: t("ui.lookupSelect.clearSelection", "Clear selection"),
|
|
961
|
-
startTypingLabel: t(
|
|
1576
|
+
startTypingLabel: t(
|
|
1577
|
+
"ui.lookupSelect.startTyping",
|
|
1578
|
+
"Start typing to search."
|
|
1579
|
+
),
|
|
962
1580
|
minQuery: 0
|
|
963
1581
|
}
|
|
964
1582
|
)
|
|
@@ -967,27 +1585,35 @@ function LineItemDialog({
|
|
|
967
1585
|
id: "name",
|
|
968
1586
|
label: t("sales.documents.items.name", "Name"),
|
|
969
1587
|
type: "text",
|
|
970
|
-
placeholder: t(
|
|
1588
|
+
placeholder: t(
|
|
1589
|
+
"sales.documents.items.namePlaceholder",
|
|
1590
|
+
"Optional line name"
|
|
1591
|
+
),
|
|
971
1592
|
layout: "full",
|
|
972
1593
|
required: isCustomLine
|
|
973
1594
|
}
|
|
974
1595
|
];
|
|
975
1596
|
}, [
|
|
1597
|
+
applyPriceSelection,
|
|
1598
|
+
convertUnitPriceForUnitChange,
|
|
976
1599
|
currencyCode,
|
|
977
1600
|
findTaxRateIdByValue,
|
|
978
1601
|
loadPrices,
|
|
1602
|
+
loadProductUnits,
|
|
979
1603
|
loadProductOptions,
|
|
980
1604
|
loadVariantOptions,
|
|
981
1605
|
fetchLineStatusItems,
|
|
982
1606
|
priceLoading,
|
|
983
1607
|
priceOptions,
|
|
984
1608
|
productOption,
|
|
1609
|
+
unitOptions,
|
|
985
1610
|
lineMode,
|
|
986
1611
|
variantOption,
|
|
987
1612
|
t,
|
|
988
1613
|
taxRateMap,
|
|
989
|
-
taxRates
|
|
1614
|
+
taxRates,
|
|
990
1615
|
resolveTaxSelection,
|
|
1616
|
+
selectPriceAfterRefresh,
|
|
991
1617
|
hasTaxMetadata
|
|
992
1618
|
]);
|
|
993
1619
|
const groups = React.useMemo(() => {
|
|
@@ -1013,11 +1639,13 @@ function LineItemDialog({
|
|
|
1013
1639
|
const snapshot = initialLine.catalogSnapshot ?? null;
|
|
1014
1640
|
const snapshotProduct = snapshot && typeof snapshot === "object" && typeof snapshot.product === "object" && snapshot.product ? snapshot.product : null;
|
|
1015
1641
|
const snapshotVariant = snapshot && typeof snapshot === "object" && typeof snapshot.variant === "object" && snapshot.variant ? snapshot.variant : null;
|
|
1016
|
-
const
|
|
1642
|
+
const metaRec = typeof meta === "object" && meta ? meta : null;
|
|
1643
|
+
const metaLineMode = typeof metaRec?.lineMode === "string" && (metaRec.lineMode === "custom" || metaRec.lineMode === "catalog") ? metaRec.lineMode : metaRec?.customLine ? "custom" : void 0;
|
|
1017
1644
|
nextForm.productId = initialLine.productId;
|
|
1018
1645
|
nextForm.variantId = initialLine.productVariantId;
|
|
1019
1646
|
nextForm.quantity = initialLine.quantity.toString();
|
|
1020
|
-
|
|
1647
|
+
nextForm.quantityUnit = normalizeUnitCode(initialLine.quantityUnit) ?? null;
|
|
1648
|
+
const metaMode = metaRec?.priceMode;
|
|
1021
1649
|
const resolvedPriceMode = metaMode === "net" || metaMode === "gross" ? metaMode : initialLine.priceMode ?? "gross";
|
|
1022
1650
|
nextForm.unitPrice = resolvedPriceMode === "net" ? initialLine.unitPriceNet.toString() : initialLine.unitPriceGross.toString();
|
|
1023
1651
|
nextForm.priceMode = resolvedPriceMode;
|
|
@@ -1027,11 +1655,13 @@ function LineItemDialog({
|
|
|
1027
1655
|
nextForm.customFieldSetId = initialLine.customFieldSetId ?? null;
|
|
1028
1656
|
nextForm.statusEntryId = initialLine.statusEntryId ?? null;
|
|
1029
1657
|
nextForm.lineMode = metaLineMode ?? (initialLine.productId || initialLine.productVariantId ? "catalog" : "custom");
|
|
1030
|
-
const metaTaxRateId = typeof
|
|
1658
|
+
const metaTaxRateId = typeof metaRec?.taxRateId === "string" ? metaRec.taxRateId : null;
|
|
1031
1659
|
const fallbackTaxRateId = findTaxRateIdByValue(nextForm.taxRate);
|
|
1032
1660
|
nextForm.taxRateId = metaTaxRateId ?? fallbackTaxRateId ?? (defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null);
|
|
1033
1661
|
if (!Number.isFinite(nextForm.taxRate) && nextForm.taxRateId) {
|
|
1034
|
-
const matched = taxRatesRef.current.find(
|
|
1662
|
+
const matched = taxRatesRef.current.find(
|
|
1663
|
+
(rate) => rate.id === nextForm.taxRateId
|
|
1664
|
+
);
|
|
1035
1665
|
const numericRate = normalizeNumber(matched?.rate);
|
|
1036
1666
|
if (Number.isFinite(numericRate)) {
|
|
1037
1667
|
nextForm.taxRate = numericRate;
|
|
@@ -1039,24 +1669,30 @@ function LineItemDialog({
|
|
|
1039
1669
|
}
|
|
1040
1670
|
let resolvedProductOption = null;
|
|
1041
1671
|
let resolvedVariantOption = null;
|
|
1042
|
-
if (
|
|
1043
|
-
const
|
|
1672
|
+
if (metaRec) {
|
|
1673
|
+
const metaRecord = metaRec;
|
|
1674
|
+
const mode = metaRecord.priceMode;
|
|
1044
1675
|
if (mode === "net" || mode === "gross") {
|
|
1045
1676
|
nextForm.priceMode = mode;
|
|
1046
1677
|
nextForm.unitPrice = mode === "net" ? initialLine.unitPriceNet.toString() : initialLine.unitPriceGross.toString();
|
|
1047
1678
|
}
|
|
1048
|
-
nextForm.priceId = typeof
|
|
1049
|
-
const productTitle = typeof
|
|
1050
|
-
const productSku = typeof
|
|
1051
|
-
const productThumbnail = typeof
|
|
1679
|
+
nextForm.priceId = typeof metaRecord.priceId === "string" ? metaRecord.priceId : null;
|
|
1680
|
+
const productTitle = typeof metaRecord.productTitle === "string" ? metaRecord.productTitle : initialLine.name;
|
|
1681
|
+
const productSku = typeof metaRecord.productSku === "string" ? metaRecord.productSku : null;
|
|
1682
|
+
const productThumbnail = typeof metaRecord.productThumbnail === "string" ? metaRecord.productThumbnail : null;
|
|
1052
1683
|
if (productTitle && initialLine.productId) {
|
|
1053
|
-
const option = {
|
|
1684
|
+
const option = {
|
|
1685
|
+
id: initialLine.productId,
|
|
1686
|
+
title: productTitle,
|
|
1687
|
+
sku: productSku,
|
|
1688
|
+
thumbnailUrl: productThumbnail
|
|
1689
|
+
};
|
|
1054
1690
|
productOptionsRef.current.set(initialLine.productId, option);
|
|
1055
1691
|
resolvedProductOption = option;
|
|
1056
1692
|
}
|
|
1057
|
-
const variantTitle = typeof
|
|
1058
|
-
const variantSku = typeof
|
|
1059
|
-
const variantThumb = typeof
|
|
1693
|
+
const variantTitle = typeof metaRecord.variantTitle === "string" ? metaRecord.variantTitle : null;
|
|
1694
|
+
const variantSku = typeof metaRecord.variantSku === "string" ? metaRecord.variantSku : null;
|
|
1695
|
+
const variantThumb = typeof metaRecord.variantThumbnail === "string" ? metaRecord.variantThumbnail : productThumbnail;
|
|
1060
1696
|
if (variantTitle && initialLine.productVariantId) {
|
|
1061
1697
|
const option = {
|
|
1062
1698
|
id: initialLine.productVariantId,
|
|
@@ -1069,32 +1705,34 @@ function LineItemDialog({
|
|
|
1069
1705
|
}
|
|
1070
1706
|
}
|
|
1071
1707
|
if (!resolvedProductOption && initialLine.productId && snapshotProduct) {
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1708
|
+
const sp = snapshotProduct;
|
|
1709
|
+
const snapshotTitle = typeof sp.title === "string" && sp.title.trim().length ? sp.title : initialLine.name ?? initialLine.productId;
|
|
1710
|
+
const snapshotSku = typeof sp.sku === "string" && sp.sku.trim().length ? sp.sku : null;
|
|
1711
|
+
const snapshotThumb = typeof sp.thumbnailUrl === "string" ? sp.thumbnailUrl : typeof sp.thumbnail_url === "string" ? sp.thumbnail_url : null;
|
|
1712
|
+
const snapshotTaxRate = normalizeNumber(sp.taxRate, Number.NaN);
|
|
1076
1713
|
const option = {
|
|
1077
1714
|
id: initialLine.productId,
|
|
1078
1715
|
title: snapshotTitle,
|
|
1079
1716
|
sku: snapshotSku,
|
|
1080
1717
|
thumbnailUrl: snapshotThumb,
|
|
1081
|
-
taxRateId: typeof
|
|
1718
|
+
taxRateId: typeof sp.taxRateId === "string" ? sp.taxRateId : null,
|
|
1082
1719
|
taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null
|
|
1083
1720
|
};
|
|
1084
1721
|
productOptionsRef.current.set(initialLine.productId, option);
|
|
1085
1722
|
resolvedProductOption = option;
|
|
1086
1723
|
}
|
|
1087
1724
|
if (!resolvedVariantOption && initialLine.productVariantId && snapshotVariant) {
|
|
1088
|
-
const
|
|
1089
|
-
const
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1725
|
+
const sv = snapshotVariant;
|
|
1726
|
+
const snapshotTitle = typeof sv.title === "string" && sv.title.trim().length ? sv.title : initialLine.name ?? initialLine.productVariantId;
|
|
1727
|
+
const snapshotSku = typeof sv.sku === "string" && sv.sku.trim().length ? sv.sku : null;
|
|
1728
|
+
const snapshotThumb = typeof sv.thumbnailUrl === "string" ? sv.thumbnailUrl : typeof sv.thumbnail_url === "string" ? sv.thumbnail_url : resolvedProductOption?.thumbnailUrl ?? productOptionsRef.current.get(initialLine.productId ?? "")?.thumbnailUrl ?? null;
|
|
1729
|
+
const snapshotTaxRate = normalizeNumber(sv.taxRate, Number.NaN);
|
|
1092
1730
|
const option = {
|
|
1093
1731
|
id: initialLine.productVariantId,
|
|
1094
1732
|
title: snapshotTitle,
|
|
1095
1733
|
sku: snapshotSku,
|
|
1096
1734
|
thumbnailUrl: snapshotThumb,
|
|
1097
|
-
taxRateId: typeof
|
|
1735
|
+
taxRateId: typeof sv.taxRateId === "string" ? sv.taxRateId : null,
|
|
1098
1736
|
taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null
|
|
1099
1737
|
};
|
|
1100
1738
|
variantOptionsRef.current.set(initialLine.productVariantId, option);
|
|
@@ -1102,51 +1740,85 @@ function LineItemDialog({
|
|
|
1102
1740
|
}
|
|
1103
1741
|
if (resolvedProductOption) setProductOption(resolvedProductOption);
|
|
1104
1742
|
if (resolvedVariantOption) setVariantOption(resolvedVariantOption);
|
|
1105
|
-
const customValues = extractCustomFieldValues(
|
|
1743
|
+
const customValues = extractCustomFieldValues(
|
|
1744
|
+
initialLine
|
|
1745
|
+
);
|
|
1106
1746
|
const merged = { ...nextForm, ...customValues };
|
|
1107
1747
|
setInitialValues(merged);
|
|
1108
1748
|
setLineMode(merged.lineMode);
|
|
1109
1749
|
setFormResetKey((prev) => prev + 1);
|
|
1110
1750
|
if (initialLine.productId) {
|
|
1111
|
-
void
|
|
1751
|
+
void loadProductUnits(initialLine.productId, resolvedProductOption).then(
|
|
1752
|
+
(options) => {
|
|
1753
|
+
const requestedUnit = normalizeUnitCode(nextForm.quantityUnit);
|
|
1754
|
+
if (requestedUnit && !options.some((entry) => entry.code === requestedUnit)) {
|
|
1755
|
+
setUnitOptions((prev) => [
|
|
1756
|
+
...prev,
|
|
1757
|
+
{ code: requestedUnit, toBaseFactor: null, isBase: false }
|
|
1758
|
+
]);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
);
|
|
1762
|
+
void loadPrices(
|
|
1763
|
+
initialLine.productId,
|
|
1764
|
+
initialLine.productVariantId,
|
|
1765
|
+
nextForm.quantity,
|
|
1766
|
+
nextForm.quantityUnit
|
|
1767
|
+
);
|
|
1112
1768
|
} else {
|
|
1113
1769
|
setPriceOptions([]);
|
|
1770
|
+
setUnitOptions([]);
|
|
1114
1771
|
}
|
|
1115
|
-
}, [
|
|
1116
|
-
|
|
1117
|
-
|
|
1772
|
+
}, [
|
|
1773
|
+
currencyCode,
|
|
1774
|
+
findTaxRateIdByValue,
|
|
1775
|
+
initialLine,
|
|
1776
|
+
loadPrices,
|
|
1777
|
+
loadProductUnits,
|
|
1778
|
+
open,
|
|
1779
|
+
resetForm
|
|
1780
|
+
]);
|
|
1781
|
+
return /* @__PURE__ */ jsx(
|
|
1782
|
+
Dialog,
|
|
1118
1783
|
{
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
{
|
|
1136
|
-
embedded: true,
|
|
1137
|
-
fields,
|
|
1138
|
-
groups,
|
|
1139
|
-
entityId: customFieldEntityId,
|
|
1140
|
-
initialValues,
|
|
1141
|
-
submitLabel: editingId ? t("sales.documents.items.save", "Save changes") : t("sales.documents.items.addLine", "Add item"),
|
|
1142
|
-
onSubmit: handleFormSubmit,
|
|
1143
|
-
loadingMessage: t("sales.documents.items.loading", "Loading items\u2026")
|
|
1784
|
+
open,
|
|
1785
|
+
onOpenChange: (next) => next ? onOpenChange(true) : closeDialog(),
|
|
1786
|
+
children: /* @__PURE__ */ jsxs(
|
|
1787
|
+
DialogContent,
|
|
1788
|
+
{
|
|
1789
|
+
className: "sm:max-w-5xl",
|
|
1790
|
+
ref: dialogContentRef,
|
|
1791
|
+
onKeyDown: (event) => {
|
|
1792
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
1793
|
+
event.preventDefault();
|
|
1794
|
+
dialogContentRef.current?.querySelector("form")?.requestSubmit();
|
|
1795
|
+
}
|
|
1796
|
+
if (event.key === "Escape") {
|
|
1797
|
+
event.preventDefault();
|
|
1798
|
+
closeDialog();
|
|
1799
|
+
}
|
|
1144
1800
|
},
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1801
|
+
children: [
|
|
1802
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editingId ? t("sales.documents.items.editTitle", "Edit line") : t("sales.documents.items.addTitle", "Add line") }) }),
|
|
1803
|
+
/* @__PURE__ */ jsx(
|
|
1804
|
+
CrudForm,
|
|
1805
|
+
{
|
|
1806
|
+
embedded: true,
|
|
1807
|
+
fields,
|
|
1808
|
+
groups,
|
|
1809
|
+
entityId: customFieldEntityId,
|
|
1810
|
+
initialValues,
|
|
1811
|
+
submitLabel: editingId ? t("sales.documents.items.save", "Save changes") : t("sales.documents.items.addLine", "Add item"),
|
|
1812
|
+
onSubmit: handleFormSubmit,
|
|
1813
|
+
loadingMessage: t("sales.documents.items.loading", "Loading items\u2026")
|
|
1814
|
+
},
|
|
1815
|
+
formResetKey
|
|
1816
|
+
)
|
|
1817
|
+
]
|
|
1818
|
+
}
|
|
1819
|
+
)
|
|
1148
1820
|
}
|
|
1149
|
-
)
|
|
1821
|
+
);
|
|
1150
1822
|
}
|
|
1151
1823
|
export {
|
|
1152
1824
|
LineItemDialog
|