@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,167 +1,340 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import * as React from
|
|
4
|
-
import {
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
LookupSelect,
|
|
6
|
+
type LookupSelectItem,
|
|
7
|
+
} from "@open-mercato/ui/backend/inputs";
|
|
5
8
|
import {
|
|
6
9
|
CrudForm,
|
|
7
10
|
type CrudField,
|
|
8
11
|
type CrudFormGroup,
|
|
9
12
|
type CrudCustomFieldRenderProps,
|
|
10
|
-
} from
|
|
11
|
-
import { collectCustomFieldValues } from
|
|
12
|
-
import { apiCall } from
|
|
13
|
-
import { createCrud, updateCrud } from
|
|
14
|
-
import { createCrudFormError } from
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
} from "@open-mercato/ui/backend/CrudForm";
|
|
14
|
+
import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
|
|
15
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
16
|
+
import { createCrud, updateCrud } from "@open-mercato/ui/backend/utils/crud";
|
|
17
|
+
import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
18
|
+
import {
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogHeader,
|
|
22
|
+
DialogTitle,
|
|
23
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
24
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
25
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
26
|
+
import { DollarSign, Settings } from "lucide-react";
|
|
27
|
+
import { normalizeCustomFieldValues } from "@open-mercato/shared/lib/custom-fields/normalize";
|
|
20
28
|
import {
|
|
21
29
|
DictionaryValue,
|
|
22
30
|
renderDictionaryIcon,
|
|
23
31
|
renderDictionaryColor,
|
|
24
|
-
} from
|
|
25
|
-
import { E } from
|
|
26
|
-
import { useT } from
|
|
27
|
-
import { useOrganizationScopeDetail } from
|
|
28
|
-
import { formatMoney, normalizeNumber } from
|
|
29
|
-
import type { SalesLineRecord } from
|
|
30
|
-
import {
|
|
32
|
+
} from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
|
|
33
|
+
import { E } from "#generated/entities.ids.generated";
|
|
34
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
35
|
+
import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
36
|
+
import { formatMoney, normalizeNumber } from "./lineItemUtils";
|
|
37
|
+
import type { SalesLineRecord } from "./lineItemTypes";
|
|
38
|
+
import {
|
|
39
|
+
normalizeCustomFieldSubmitValue,
|
|
40
|
+
extractCustomFieldValues,
|
|
41
|
+
} from "./customFieldHelpers";
|
|
42
|
+
import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
|
|
31
43
|
|
|
32
44
|
type ProductOption = {
|
|
33
|
-
id: string
|
|
34
|
-
title: string
|
|
35
|
-
sku: string | null
|
|
36
|
-
thumbnailUrl: string | null
|
|
37
|
-
taxRateId?: string | null
|
|
38
|
-
taxRate?: number | null
|
|
39
|
-
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
sku: string | null;
|
|
48
|
+
thumbnailUrl: string | null;
|
|
49
|
+
taxRateId?: string | null;
|
|
50
|
+
taxRate?: number | null;
|
|
51
|
+
defaultUnit?: string | null;
|
|
52
|
+
defaultSalesUnit?: string | null;
|
|
53
|
+
defaultSalesUnitQuantity?: number | null;
|
|
54
|
+
};
|
|
40
55
|
|
|
41
56
|
type VariantOption = {
|
|
42
|
-
id: string
|
|
43
|
-
title: string
|
|
44
|
-
sku: string | null
|
|
45
|
-
thumbnailUrl: string | null
|
|
46
|
-
taxRateId?: string | null
|
|
47
|
-
taxRate?: number | null
|
|
48
|
-
}
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
sku: string | null;
|
|
60
|
+
thumbnailUrl: string | null;
|
|
61
|
+
taxRateId?: string | null;
|
|
62
|
+
taxRate?: number | null;
|
|
63
|
+
};
|
|
49
64
|
|
|
50
65
|
type PriceOption = {
|
|
51
|
-
id: string
|
|
52
|
-
amountNet: number | null
|
|
53
|
-
amountGross: number | null
|
|
54
|
-
currencyCode: string | null
|
|
55
|
-
displayMode:
|
|
56
|
-
taxRate: number | null
|
|
57
|
-
label: string
|
|
58
|
-
priceKindId?: string | null
|
|
59
|
-
priceKindTitle?: string | null
|
|
60
|
-
priceKindCode?: string | null
|
|
61
|
-
scopeReason?: string | null
|
|
62
|
-
scopeTags?: string[]
|
|
63
|
-
}
|
|
66
|
+
id: string;
|
|
67
|
+
amountNet: number | null;
|
|
68
|
+
amountGross: number | null;
|
|
69
|
+
currencyCode: string | null;
|
|
70
|
+
displayMode: "including-tax" | "excluding-tax" | null;
|
|
71
|
+
taxRate: number | null;
|
|
72
|
+
label: string;
|
|
73
|
+
priceKindId?: string | null;
|
|
74
|
+
priceKindTitle?: string | null;
|
|
75
|
+
priceKindCode?: string | null;
|
|
76
|
+
scopeReason?: string | null;
|
|
77
|
+
scopeTags?: string[];
|
|
78
|
+
};
|
|
64
79
|
|
|
65
80
|
type TaxRateOption = {
|
|
66
|
-
id: string
|
|
67
|
-
name: string
|
|
68
|
-
code: string | null
|
|
69
|
-
rate: number | null
|
|
70
|
-
isDefault: boolean
|
|
71
|
-
}
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
code: string | null;
|
|
84
|
+
rate: number | null;
|
|
85
|
+
isDefault: boolean;
|
|
86
|
+
};
|
|
72
87
|
|
|
73
88
|
type StatusOption = {
|
|
74
|
-
id: string
|
|
75
|
-
value: string
|
|
76
|
-
label: string
|
|
77
|
-
color: string | null
|
|
78
|
-
icon: string | null
|
|
79
|
-
}
|
|
89
|
+
id: string;
|
|
90
|
+
value: string;
|
|
91
|
+
label: string;
|
|
92
|
+
color: string | null;
|
|
93
|
+
icon: string | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type UnitOption = {
|
|
97
|
+
code: string;
|
|
98
|
+
toBaseFactor: number | null;
|
|
99
|
+
isBase: boolean;
|
|
100
|
+
};
|
|
80
101
|
|
|
81
102
|
type LineFormState = {
|
|
82
|
-
lineMode:
|
|
83
|
-
productId: string | null
|
|
84
|
-
variantId: string | null
|
|
85
|
-
quantity: string
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
lineMode: "catalog" | "custom";
|
|
104
|
+
productId: string | null;
|
|
105
|
+
variantId: string | null;
|
|
106
|
+
quantity: string;
|
|
107
|
+
quantityUnit: string | null;
|
|
108
|
+
priceId: string | null;
|
|
109
|
+
priceMode: "net" | "gross";
|
|
110
|
+
unitPrice: string;
|
|
111
|
+
taxRate: number | null;
|
|
112
|
+
taxRateId: string | null;
|
|
113
|
+
name: string;
|
|
114
|
+
currencyCode: string | null;
|
|
115
|
+
catalogSnapshot?: Record<string, unknown> | null;
|
|
116
|
+
customFieldSetId?: string | null;
|
|
117
|
+
statusEntryId?: string | null;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type FieldRenderProps = CrudCustomFieldRenderProps;
|
|
121
|
+
|
|
122
|
+
type ApiPriceKind = {
|
|
123
|
+
title?: string | null;
|
|
124
|
+
name?: string | null;
|
|
125
|
+
code?: string | null;
|
|
126
|
+
};
|
|
97
127
|
|
|
98
|
-
type
|
|
128
|
+
type ApiPriceItem = Record<string, unknown> & {
|
|
129
|
+
unit_price_net?: number | null;
|
|
130
|
+
unit_price_gross?: number | null;
|
|
131
|
+
currency_code?: string | null;
|
|
132
|
+
currencyCode?: string | null;
|
|
133
|
+
display_mode?: string | null;
|
|
134
|
+
displayMode?: string | null;
|
|
135
|
+
tax_rate?: number | null;
|
|
136
|
+
price_kind_id?: string | null;
|
|
137
|
+
priceKindId?: string | null;
|
|
138
|
+
price_kind_title?: string | null;
|
|
139
|
+
priceKindTitle?: string | null;
|
|
140
|
+
price_kind_code?: string | null;
|
|
141
|
+
priceKindCode?: string | null;
|
|
142
|
+
price_kind?: ApiPriceKind | null;
|
|
143
|
+
kind?: string | null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type ApiTaxRateItem = Record<string, unknown> & {
|
|
147
|
+
rate?: number | null;
|
|
148
|
+
code?: string | null;
|
|
149
|
+
isDefault?: boolean;
|
|
150
|
+
is_default?: boolean;
|
|
151
|
+
min_quantity?: number | null;
|
|
152
|
+
max_quantity?: number | null;
|
|
153
|
+
starts_at?: string | null;
|
|
154
|
+
ends_at?: string | null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type ApiProductItem = Record<string, unknown> & {
|
|
158
|
+
name?: string;
|
|
159
|
+
sku?: string;
|
|
160
|
+
default_media_url?: string;
|
|
161
|
+
defaultMediaUrl?: string;
|
|
162
|
+
pricing?: Record<string, unknown> | null;
|
|
163
|
+
metadata?: Record<string, unknown> | null;
|
|
164
|
+
tax_rate?: number | null;
|
|
165
|
+
taxRate?: number | null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
type ApiVariantItem = Record<string, unknown> & {
|
|
169
|
+
sku?: string;
|
|
170
|
+
default_media_url?: string;
|
|
171
|
+
thumbnailUrl?: string;
|
|
172
|
+
metadata?: Record<string, unknown> | null;
|
|
173
|
+
tax_rate?: number | null;
|
|
174
|
+
taxRate?: number | null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
type ApiPricingMetadata = {
|
|
178
|
+
tax_rate_id?: string;
|
|
179
|
+
taxRateId?: string;
|
|
180
|
+
tax_rate?: number | null;
|
|
181
|
+
taxRate?: number | null;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type LineMetadataRecord = {
|
|
185
|
+
lineMode?: string;
|
|
186
|
+
customLine?: boolean | null;
|
|
187
|
+
priceMode?: string;
|
|
188
|
+
taxRateId?: string;
|
|
189
|
+
priceId?: string;
|
|
190
|
+
productTitle?: string;
|
|
191
|
+
productSku?: string;
|
|
192
|
+
productThumbnail?: string;
|
|
193
|
+
variantTitle?: string;
|
|
194
|
+
variantSku?: string;
|
|
195
|
+
variantThumbnail?: string;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
type CatalogSnapshotRecord = Record<string, unknown> & {
|
|
199
|
+
product?: Record<string, unknown> | null;
|
|
200
|
+
variant?: Record<string, unknown> | null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
type SnapshotEntity = {
|
|
204
|
+
title?: string;
|
|
205
|
+
sku?: string;
|
|
206
|
+
thumbnailUrl?: string;
|
|
207
|
+
thumbnail_url?: string;
|
|
208
|
+
taxRate?: number | null;
|
|
209
|
+
taxRateId?: string;
|
|
210
|
+
};
|
|
99
211
|
|
|
100
212
|
type SalesLineDialogProps = {
|
|
101
|
-
open: boolean
|
|
102
|
-
kind:
|
|
103
|
-
documentId: string
|
|
104
|
-
currencyCode: string | null | undefined
|
|
105
|
-
organizationId: string | null
|
|
106
|
-
tenantId: string | null
|
|
107
|
-
initialLine?: SalesLineRecord | null
|
|
108
|
-
onOpenChange: (open: boolean) => void
|
|
109
|
-
onSaved?: () => Promise<void> | void
|
|
110
|
-
}
|
|
213
|
+
open: boolean;
|
|
214
|
+
kind: "order" | "quote";
|
|
215
|
+
documentId: string;
|
|
216
|
+
currencyCode: string | null | undefined;
|
|
217
|
+
organizationId: string | null;
|
|
218
|
+
tenantId: string | null;
|
|
219
|
+
initialLine?: SalesLineRecord | null;
|
|
220
|
+
onOpenChange: (open: boolean) => void;
|
|
221
|
+
onSaved?: () => Promise<void> | void;
|
|
222
|
+
};
|
|
111
223
|
|
|
112
224
|
const defaultForm = (currencyCode?: string | null): LineFormState => ({
|
|
113
|
-
lineMode:
|
|
225
|
+
lineMode: "catalog",
|
|
114
226
|
productId: null,
|
|
115
227
|
variantId: null,
|
|
116
|
-
quantity:
|
|
228
|
+
quantity: "1",
|
|
229
|
+
quantityUnit: null,
|
|
117
230
|
priceId: null,
|
|
118
|
-
priceMode:
|
|
119
|
-
unitPrice:
|
|
231
|
+
priceMode: "gross",
|
|
232
|
+
unitPrice: "",
|
|
120
233
|
taxRate: null,
|
|
121
234
|
taxRateId: null,
|
|
122
|
-
name:
|
|
235
|
+
name: "",
|
|
123
236
|
currencyCode: currencyCode ?? null,
|
|
124
237
|
catalogSnapshot: null,
|
|
125
238
|
customFieldSetId: null,
|
|
126
239
|
statusEntryId: null,
|
|
127
|
-
})
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const UNIT_PRICE_INPUT_SCALE = 4;
|
|
128
243
|
|
|
129
|
-
function buildPriceScopeReason(
|
|
130
|
-
|
|
131
|
-
|
|
244
|
+
function buildPriceScopeReason(
|
|
245
|
+
item: Record<string, unknown>,
|
|
246
|
+
t: (k: string, f: string) => string,
|
|
247
|
+
): {
|
|
248
|
+
reason: string | null;
|
|
249
|
+
tags: string[];
|
|
132
250
|
} {
|
|
133
|
-
const tags: string[] = []
|
|
134
|
-
const add = (key: string) => tags.push(key)
|
|
135
|
-
if (item.channel_id || item.channelId)
|
|
136
|
-
|
|
137
|
-
if (item.
|
|
138
|
-
|
|
139
|
-
if (item.
|
|
140
|
-
|
|
141
|
-
if (item.
|
|
142
|
-
|
|
143
|
-
|
|
251
|
+
const tags: string[] = [];
|
|
252
|
+
const add = (key: string) => tags.push(key);
|
|
253
|
+
if (item.channel_id || item.channelId)
|
|
254
|
+
add(t("sales.documents.items.priceScope.channel", "Channel"));
|
|
255
|
+
if (item.offer_id || item.offerId)
|
|
256
|
+
add(t("sales.documents.items.priceScope.offer", "Offer"));
|
|
257
|
+
if (item.variant_id || item.variantId)
|
|
258
|
+
add(t("sales.documents.items.priceScope.variant", "Variant"));
|
|
259
|
+
if (item.customer_group_id || item.customerGroupId)
|
|
260
|
+
add(t("sales.documents.items.priceScope.customerGroup", "Customer group"));
|
|
261
|
+
if (item.customer_id || item.customerId)
|
|
262
|
+
add(t("sales.documents.items.priceScope.customer", "Customer"));
|
|
263
|
+
if (item.user_group_id || item.userGroupId)
|
|
264
|
+
add(t("sales.documents.items.priceScope.userGroup", "User group"));
|
|
265
|
+
if (item.user_id || item.userId)
|
|
266
|
+
add(t("sales.documents.items.priceScope.user", "User"));
|
|
267
|
+
const minQty = normalizeNumber((item as ApiPriceItem).min_quantity, Number.NaN);
|
|
268
|
+
const maxQty = normalizeNumber((item as ApiPriceItem).max_quantity, Number.NaN);
|
|
144
269
|
if (Number.isFinite(minQty) || Number.isFinite(maxQty)) {
|
|
145
|
-
add(
|
|
146
|
-
t(
|
|
147
|
-
'sales.documents.items.priceScope.quantity',
|
|
148
|
-
'Quantity',
|
|
149
|
-
),
|
|
150
|
-
)
|
|
270
|
+
add(t("sales.documents.items.priceScope.quantity", "Quantity"));
|
|
151
271
|
}
|
|
152
|
-
if ((item as
|
|
153
|
-
add(t(
|
|
272
|
+
if ((item as ApiPriceItem).starts_at || (item as ApiPriceItem).ends_at) {
|
|
273
|
+
add(t("sales.documents.items.priceScope.schedule", "Scheduled"));
|
|
154
274
|
}
|
|
155
|
-
if (tags.length === 0) return { reason: null, tags }
|
|
156
|
-
return { reason: tags.join(
|
|
275
|
+
if (tags.length === 0) return { reason: null, tags };
|
|
276
|
+
return { reason: tags.join(" • "), tags };
|
|
157
277
|
}
|
|
158
278
|
|
|
159
279
|
function buildPlaceholder(label?: string | null) {
|
|
160
280
|
return (
|
|
161
281
|
<div className="flex h-8 w-8 items-center justify-center rounded border bg-muted text-[10px] uppercase text-muted-foreground">
|
|
162
|
-
{(label ??
|
|
282
|
+
{(label ?? "").slice(0, 2) || "•"}
|
|
163
283
|
</div>
|
|
164
|
-
)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeUnitCode(value: unknown): string | null {
|
|
288
|
+
return canonicalizeUnitCode(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getRecordBoolean(
|
|
292
|
+
record: Record<string, unknown>,
|
|
293
|
+
fallback: boolean,
|
|
294
|
+
...keys: string[]
|
|
295
|
+
): boolean {
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
const val = record[key];
|
|
298
|
+
if (typeof val === "boolean") return val;
|
|
299
|
+
}
|
|
300
|
+
return fallback;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getUomProductFields(item: Record<string, unknown>) {
|
|
304
|
+
return {
|
|
305
|
+
defaultUnit: normalizeUnitCode(item.default_unit ?? item.defaultUnit),
|
|
306
|
+
defaultSalesUnit: normalizeUnitCode(
|
|
307
|
+
item.default_sales_unit ?? item.defaultSalesUnit,
|
|
308
|
+
),
|
|
309
|
+
defaultSalesUnitQuantity: normalizeNumber(
|
|
310
|
+
item.default_sales_unit_quantity ?? item.defaultSalesUnitQuantity,
|
|
311
|
+
Number.NaN,
|
|
312
|
+
),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getUomConversionFields(row: Record<string, unknown>) {
|
|
317
|
+
return {
|
|
318
|
+
unitCode: normalizeUnitCode(row.unit_code ?? row.unitCode),
|
|
319
|
+
isActive: getRecordBoolean(row, true, "is_active", "isActive"),
|
|
320
|
+
toBaseFactor: normalizeNumber(
|
|
321
|
+
row.to_base_factor ?? row.toBaseFactor,
|
|
322
|
+
Number.NaN,
|
|
323
|
+
),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeQuantityPreview(value: number): number {
|
|
328
|
+
if (!Number.isFinite(value)) return 0;
|
|
329
|
+
return Math.round(value * 1_000_000) / 1_000_000;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeUnitPriceInputValue(value: number): string {
|
|
333
|
+
if (!Number.isFinite(value)) return "";
|
|
334
|
+
const factor = 10 ** UNIT_PRICE_INPUT_SCALE;
|
|
335
|
+
const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
|
|
336
|
+
if (!Number.isFinite(rounded)) return "";
|
|
337
|
+
return rounded.toString();
|
|
165
338
|
}
|
|
166
339
|
|
|
167
340
|
export function LineItemDialog({
|
|
@@ -175,209 +348,271 @@ export function LineItemDialog({
|
|
|
175
348
|
onOpenChange,
|
|
176
349
|
onSaved,
|
|
177
350
|
}: SalesLineDialogProps) {
|
|
178
|
-
const t = useT()
|
|
179
|
-
const scope = useOrganizationScopeDetail()
|
|
180
|
-
const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null
|
|
181
|
-
const resolvedTenantId = tenantId ?? scope.tenantId ?? null
|
|
182
|
-
const [initialValues, setInitialValues] = React.useState<LineFormState>(() =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const [
|
|
189
|
-
|
|
190
|
-
const [
|
|
191
|
-
|
|
192
|
-
const [,
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
351
|
+
const t = useT();
|
|
352
|
+
const scope = useOrganizationScopeDetail();
|
|
353
|
+
const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null;
|
|
354
|
+
const resolvedTenantId = tenantId ?? scope.tenantId ?? null;
|
|
355
|
+
const [initialValues, setInitialValues] = React.useState<LineFormState>(() =>
|
|
356
|
+
defaultForm(currencyCode),
|
|
357
|
+
);
|
|
358
|
+
const [lineMode, setLineMode] = React.useState<"catalog" | "custom">(
|
|
359
|
+
defaultForm(currencyCode).lineMode,
|
|
360
|
+
);
|
|
361
|
+
const [productOption, setProductOption] =
|
|
362
|
+
React.useState<ProductOption | null>(null);
|
|
363
|
+
const [variantOption, setVariantOption] =
|
|
364
|
+
React.useState<VariantOption | null>(null);
|
|
365
|
+
const [priceOptions, setPriceOptions] = React.useState<PriceOption[]>([]);
|
|
366
|
+
const [priceLoading, setPriceLoading] = React.useState(false);
|
|
367
|
+
const [formResetKey, setFormResetKey] = React.useState(0);
|
|
368
|
+
const [editingId, setEditingId] = React.useState<string | null>(null);
|
|
369
|
+
const [taxRates, setTaxRates] = React.useState<TaxRateOption[]>([]);
|
|
370
|
+
const [lineStatuses, setLineStatuses] = React.useState<StatusOption[]>([]);
|
|
371
|
+
const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);
|
|
372
|
+
const [, setLineStatusLoading] = React.useState(false);
|
|
373
|
+
const productOptionsRef = React.useRef<Map<string, ProductOption>>(new Map());
|
|
374
|
+
const variantOptionsRef = React.useRef<Map<string, VariantOption>>(new Map());
|
|
375
|
+
const taxRatesRef = React.useRef<TaxRateOption[]>([]);
|
|
376
|
+
const defaultTaxRateRef = React.useRef<TaxRateOption | null>(null);
|
|
377
|
+
const dialogContentRef = React.useRef<HTMLDivElement | null>(null);
|
|
198
378
|
|
|
199
379
|
const resourcePath = React.useMemo(
|
|
200
|
-
() => (kind ===
|
|
380
|
+
() => (kind === "order" ? "sales/order-lines" : "sales/quote-lines"),
|
|
201
381
|
[kind],
|
|
202
|
-
)
|
|
203
|
-
const documentKey = kind ===
|
|
204
|
-
const customFieldEntityId =
|
|
382
|
+
);
|
|
383
|
+
const documentKey = kind === "order" ? "orderId" : "quoteId";
|
|
384
|
+
const customFieldEntityId =
|
|
385
|
+
kind === "order" ? E.sales.sales_order_line : E.sales.sales_quote_line;
|
|
205
386
|
|
|
206
387
|
const taxRateMap = React.useMemo(
|
|
207
388
|
() =>
|
|
208
389
|
taxRates.reduce<Map<string, TaxRateOption>>((acc, rate) => {
|
|
209
|
-
acc.set(rate.id, rate)
|
|
210
|
-
return acc
|
|
390
|
+
acc.set(rate.id, rate);
|
|
391
|
+
return acc;
|
|
211
392
|
}, new Map()),
|
|
212
|
-
[taxRates]
|
|
213
|
-
)
|
|
393
|
+
[taxRates],
|
|
394
|
+
);
|
|
214
395
|
|
|
215
396
|
const findTaxRateIdByValue = React.useCallback(
|
|
216
397
|
(value: number | null | undefined): string | null => {
|
|
217
|
-
const numeric = normalizeNumber(value, Number.NaN)
|
|
218
|
-
if (!Number.isFinite(numeric)) return null
|
|
398
|
+
const numeric = normalizeNumber(value, Number.NaN);
|
|
399
|
+
if (!Number.isFinite(numeric)) return null;
|
|
219
400
|
const match = taxRatesRef.current.find(
|
|
220
|
-
(rate) =>
|
|
221
|
-
|
|
222
|
-
|
|
401
|
+
(rate) =>
|
|
402
|
+
Number.isFinite(rate.rate) &&
|
|
403
|
+
Math.abs((rate.rate as number) - numeric) < 0.0001,
|
|
404
|
+
);
|
|
405
|
+
return match?.id ?? null;
|
|
223
406
|
},
|
|
224
|
-
[]
|
|
225
|
-
)
|
|
407
|
+
[],
|
|
408
|
+
);
|
|
226
409
|
|
|
227
410
|
const resolveTaxSelection = React.useCallback(
|
|
228
|
-
(
|
|
411
|
+
(
|
|
412
|
+
source?: { taxRateId?: string | null; taxRate?: number | null } | null,
|
|
413
|
+
) => {
|
|
229
414
|
const taxRateId =
|
|
230
|
-
typeof source?.taxRateId ===
|
|
231
|
-
|
|
232
|
-
|
|
415
|
+
typeof source?.taxRateId === "string" && source.taxRateId.trim().length
|
|
416
|
+
? source.taxRateId.trim()
|
|
417
|
+
: null;
|
|
418
|
+
const rateFromId = taxRateId
|
|
419
|
+
? normalizeNumber(taxRateMap.get(taxRateId)?.rate, Number.NaN)
|
|
420
|
+
: Number.NaN;
|
|
421
|
+
const numericRate = normalizeNumber(source?.taxRate, Number.NaN);
|
|
233
422
|
const resolvedRateId =
|
|
234
423
|
taxRateId ??
|
|
235
|
-
(Number.isFinite(numericRate)
|
|
424
|
+
(Number.isFinite(numericRate)
|
|
425
|
+
? findTaxRateIdByValue(numericRate)
|
|
426
|
+
: null);
|
|
236
427
|
const resolvedRate = Number.isFinite(rateFromId)
|
|
237
428
|
? rateFromId
|
|
238
429
|
: Number.isFinite(numericRate)
|
|
239
430
|
? numericRate
|
|
240
|
-
: null
|
|
241
|
-
return { taxRateId: resolvedRateId, taxRate: resolvedRate }
|
|
431
|
+
: null;
|
|
432
|
+
return { taxRateId: resolvedRateId, taxRate: resolvedRate };
|
|
242
433
|
},
|
|
243
|
-
[findTaxRateIdByValue, taxRateMap]
|
|
244
|
-
)
|
|
434
|
+
[findTaxRateIdByValue, taxRateMap],
|
|
435
|
+
);
|
|
245
436
|
|
|
246
437
|
const hasTaxMetadata = React.useCallback(
|
|
247
|
-
(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
const
|
|
252
|
-
|
|
438
|
+
(
|
|
439
|
+
source?: { taxRateId?: string | null; taxRate?: number | null } | null,
|
|
440
|
+
) => {
|
|
441
|
+
if (!source) return false;
|
|
442
|
+
const id =
|
|
443
|
+
typeof source.taxRateId === "string" ? source.taxRateId.trim() : "";
|
|
444
|
+
if (id.length) return true;
|
|
445
|
+
const numericRate = normalizeNumber(source.taxRate, Number.NaN);
|
|
446
|
+
return Number.isFinite(numericRate);
|
|
253
447
|
},
|
|
254
|
-
[]
|
|
255
|
-
)
|
|
448
|
+
[],
|
|
449
|
+
);
|
|
256
450
|
|
|
257
451
|
const resetForm = React.useCallback(
|
|
258
452
|
(next?: Partial<LineFormState>) => {
|
|
259
|
-
const base = { ...defaultForm(currencyCode), ...next }
|
|
260
|
-
const defaultRate = defaultTaxRateRef.current
|
|
453
|
+
const base = { ...defaultForm(currencyCode), ...next };
|
|
454
|
+
const defaultRate = defaultTaxRateRef.current;
|
|
261
455
|
if (!base.taxRateId && defaultRate) {
|
|
262
|
-
base.taxRateId = defaultRate.id
|
|
456
|
+
base.taxRateId = defaultRate.id;
|
|
263
457
|
base.taxRate = Number.isFinite(defaultRate.rate ?? null)
|
|
264
458
|
? (defaultRate.rate as number)
|
|
265
|
-
: base.taxRate
|
|
459
|
+
: base.taxRate;
|
|
266
460
|
}
|
|
267
|
-
setInitialValues(base)
|
|
268
|
-
setLineMode(base.lineMode)
|
|
269
|
-
setProductOption(null)
|
|
270
|
-
setVariantOption(null)
|
|
271
|
-
setPriceOptions([])
|
|
272
|
-
|
|
273
|
-
|
|
461
|
+
setInitialValues(base);
|
|
462
|
+
setLineMode(base.lineMode);
|
|
463
|
+
setProductOption(null);
|
|
464
|
+
setVariantOption(null);
|
|
465
|
+
setPriceOptions([]);
|
|
466
|
+
setUnitOptions([]);
|
|
467
|
+
setEditingId(null);
|
|
468
|
+
setFormResetKey((prev) => prev + 1);
|
|
274
469
|
},
|
|
275
470
|
[currencyCode],
|
|
276
|
-
)
|
|
471
|
+
);
|
|
277
472
|
|
|
278
473
|
const closeDialog = React.useCallback(() => {
|
|
279
|
-
onOpenChange(false)
|
|
280
|
-
resetForm()
|
|
281
|
-
}, [onOpenChange, resetForm])
|
|
474
|
+
onOpenChange(false);
|
|
475
|
+
resetForm();
|
|
476
|
+
}, [onOpenChange, resetForm]);
|
|
282
477
|
|
|
283
478
|
const loadTaxRates = React.useCallback(async () => {
|
|
284
479
|
try {
|
|
285
|
-
const response = await apiCall<{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
290
|
-
const items = Array.isArray(response.result?.items)
|
|
480
|
+
const response = await apiCall<{
|
|
481
|
+
items?: Array<Record<string, unknown>>;
|
|
482
|
+
}>("/api/sales/tax-rates?pageSize=100", undefined, {
|
|
483
|
+
fallback: { items: [] },
|
|
484
|
+
});
|
|
485
|
+
const items = Array.isArray(response.result?.items)
|
|
486
|
+
? response.result.items
|
|
487
|
+
: [];
|
|
291
488
|
const parsed = items
|
|
292
489
|
.map<TaxRateOption | null>((item) => {
|
|
293
|
-
const id = typeof item.id ===
|
|
490
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
294
491
|
const name =
|
|
295
|
-
typeof item.name ===
|
|
492
|
+
typeof item.name === "string" && item.name.trim().length
|
|
296
493
|
? item.name.trim()
|
|
297
|
-
: typeof item.code ===
|
|
494
|
+
: typeof item.code === "string"
|
|
298
495
|
? item.code
|
|
299
|
-
: null
|
|
300
|
-
if (!id || !name) return null
|
|
301
|
-
const rate = normalizeNumber((item as
|
|
496
|
+
: null;
|
|
497
|
+
if (!id || !name) return null;
|
|
498
|
+
const rate = normalizeNumber((item as ApiTaxRateItem).rate);
|
|
302
499
|
const code =
|
|
303
|
-
typeof (item as
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
500
|
+
typeof (item as ApiTaxRateItem).code === "string" &&
|
|
501
|
+
(item as ApiTaxRateItem).code?.trim().length
|
|
502
|
+
? (item as ApiTaxRateItem).code?.trim() ?? null
|
|
503
|
+
: null;
|
|
504
|
+
const isDefault = Boolean(
|
|
505
|
+
(item as ApiTaxRateItem).isDefault ?? (item as ApiTaxRateItem).is_default,
|
|
506
|
+
);
|
|
507
|
+
return {
|
|
508
|
+
id,
|
|
509
|
+
name,
|
|
510
|
+
code,
|
|
511
|
+
rate: Number.isFinite(rate) ? rate : null,
|
|
512
|
+
isDefault,
|
|
513
|
+
};
|
|
308
514
|
})
|
|
309
|
-
.filter((entry): entry is TaxRateOption => Boolean(entry))
|
|
310
|
-
taxRatesRef.current = parsed
|
|
311
|
-
defaultTaxRateRef.current = parsed.find((rate) => rate.isDefault) ?? null
|
|
312
|
-
setTaxRates(parsed)
|
|
313
|
-
return parsed
|
|
515
|
+
.filter((entry): entry is TaxRateOption => Boolean(entry));
|
|
516
|
+
taxRatesRef.current = parsed;
|
|
517
|
+
defaultTaxRateRef.current = parsed.find((rate) => rate.isDefault) ?? null;
|
|
518
|
+
setTaxRates(parsed);
|
|
519
|
+
return parsed;
|
|
314
520
|
} catch (err) {
|
|
315
|
-
console.error(
|
|
316
|
-
taxRatesRef.current = []
|
|
317
|
-
defaultTaxRateRef.current = null
|
|
318
|
-
setTaxRates([])
|
|
319
|
-
return []
|
|
521
|
+
console.error("sales.tax-rates.fetch", err);
|
|
522
|
+
taxRatesRef.current = [];
|
|
523
|
+
defaultTaxRateRef.current = null;
|
|
524
|
+
setTaxRates([]);
|
|
525
|
+
return [];
|
|
320
526
|
}
|
|
321
|
-
}, [])
|
|
527
|
+
}, []);
|
|
322
528
|
|
|
323
529
|
const loadProductOptions = React.useCallback(
|
|
324
530
|
async (query?: string): Promise<LookupSelectItem[]> => {
|
|
325
|
-
const params = new URLSearchParams({ pageSize:
|
|
326
|
-
if (query && query.trim().length) params.set(
|
|
327
|
-
const response = await apiCall<{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
332
|
-
const items = Array.isArray(response.result?.items)
|
|
333
|
-
|
|
531
|
+
const params = new URLSearchParams({ pageSize: "8" });
|
|
532
|
+
if (query && query.trim().length) params.set("search", query.trim());
|
|
533
|
+
const response = await apiCall<{
|
|
534
|
+
items?: Array<Record<string, unknown>>;
|
|
535
|
+
}>(`/api/catalog/products?${params.toString()}`, undefined, {
|
|
536
|
+
fallback: { items: [] },
|
|
537
|
+
});
|
|
538
|
+
const items = Array.isArray(response.result?.items)
|
|
539
|
+
? (response.result?.items ?? [])
|
|
540
|
+
: [];
|
|
541
|
+
const needle = query?.trim().toLowerCase() ?? "";
|
|
334
542
|
return items
|
|
335
543
|
.map((item) => {
|
|
336
|
-
const id = typeof item.id ===
|
|
337
|
-
if (!id) return null
|
|
544
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
545
|
+
if (!id) return null;
|
|
546
|
+
const productItem = item as ApiProductItem;
|
|
338
547
|
const title =
|
|
339
|
-
typeof item.title ===
|
|
548
|
+
typeof item.title === "string"
|
|
340
549
|
? item.title
|
|
341
|
-
: typeof
|
|
342
|
-
?
|
|
343
|
-
: id
|
|
344
|
-
const sku =
|
|
550
|
+
: typeof productItem.name === "string"
|
|
551
|
+
? productItem.name
|
|
552
|
+
: id;
|
|
553
|
+
const sku =
|
|
554
|
+
typeof productItem.sku === "string" ? productItem.sku : null;
|
|
345
555
|
const thumbnail =
|
|
346
|
-
typeof
|
|
347
|
-
?
|
|
348
|
-
: typeof
|
|
349
|
-
?
|
|
350
|
-
: null
|
|
351
|
-
const pricing =
|
|
352
|
-
|
|
556
|
+
typeof productItem.default_media_url === "string"
|
|
557
|
+
? productItem.default_media_url
|
|
558
|
+
: typeof productItem.defaultMediaUrl === "string"
|
|
559
|
+
? productItem.defaultMediaUrl
|
|
560
|
+
: null;
|
|
561
|
+
const pricing =
|
|
562
|
+
typeof productItem.pricing === "object" && productItem.pricing
|
|
563
|
+
? productItem.pricing
|
|
564
|
+
: null;
|
|
565
|
+
const metadata =
|
|
566
|
+
typeof productItem.metadata === "object" && productItem.metadata
|
|
567
|
+
? productItem.metadata
|
|
568
|
+
: null;
|
|
569
|
+
const pricingMeta = pricing as ApiPricingMetadata | null;
|
|
570
|
+
const metaMeta = metadata as ApiPricingMetadata | null;
|
|
353
571
|
const pricingTaxRateId =
|
|
354
|
-
typeof
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
572
|
+
typeof pricingMeta?.tax_rate_id === "string" &&
|
|
573
|
+
pricingMeta.tax_rate_id.trim().length
|
|
574
|
+
? pricingMeta.tax_rate_id.trim()
|
|
575
|
+
: typeof pricingMeta?.taxRateId === "string" &&
|
|
576
|
+
pricingMeta.taxRateId.trim().length
|
|
577
|
+
? pricingMeta.taxRateId.trim()
|
|
578
|
+
: null;
|
|
359
579
|
const metaTaxRateId =
|
|
360
|
-
typeof
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
580
|
+
typeof metaMeta?.taxRateId === "string" &&
|
|
581
|
+
metaMeta.taxRateId.trim().length
|
|
582
|
+
? metaMeta.taxRateId.trim()
|
|
583
|
+
: typeof metaMeta?.tax_rate_id === "string" &&
|
|
584
|
+
metaMeta.tax_rate_id.trim().length
|
|
585
|
+
? metaMeta.tax_rate_id.trim()
|
|
586
|
+
: null;
|
|
365
587
|
const taxRateValue = normalizeNumber(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
588
|
+
pricingMeta?.tax_rate ??
|
|
589
|
+
pricingMeta?.taxRate ??
|
|
590
|
+
productItem.tax_rate ??
|
|
591
|
+
productItem.taxRate,
|
|
592
|
+
Number.NaN,
|
|
593
|
+
);
|
|
594
|
+
const uomFields = getUomProductFields(item);
|
|
595
|
+
const defaultUnit = uomFields.defaultUnit;
|
|
596
|
+
const defaultSalesUnit = uomFields.defaultSalesUnit;
|
|
597
|
+
const defaultSalesUnitQuantity = uomFields.defaultSalesUnitQuantity;
|
|
369
598
|
const matches =
|
|
370
599
|
!needle ||
|
|
371
600
|
title.toLowerCase().includes(needle) ||
|
|
372
|
-
(sku ? sku.toLowerCase().includes(needle) : false)
|
|
373
|
-
if (!matches) return null
|
|
601
|
+
(sku ? sku.toLowerCase().includes(needle) : false);
|
|
602
|
+
if (!matches) return null;
|
|
374
603
|
return {
|
|
375
604
|
id,
|
|
376
605
|
title,
|
|
377
606
|
subtitle: sku ?? undefined,
|
|
378
|
-
icon: thumbnail
|
|
379
|
-
|
|
380
|
-
|
|
607
|
+
icon: thumbnail ? (
|
|
608
|
+
<img
|
|
609
|
+
src={thumbnail}
|
|
610
|
+
alt={title}
|
|
611
|
+
className="h-8 w-8 rounded object-cover"
|
|
612
|
+
/>
|
|
613
|
+
) : (
|
|
614
|
+
buildPlaceholder(title)
|
|
615
|
+
),
|
|
381
616
|
option: {
|
|
382
617
|
id,
|
|
383
618
|
title,
|
|
@@ -385,57 +620,91 @@ export function LineItemDialog({
|
|
|
385
620
|
thumbnailUrl: thumbnail,
|
|
386
621
|
taxRateId: pricingTaxRateId ?? metaTaxRateId ?? null,
|
|
387
622
|
taxRate: Number.isFinite(taxRateValue) ? taxRateValue : null,
|
|
623
|
+
defaultUnit,
|
|
624
|
+
defaultSalesUnit,
|
|
625
|
+
defaultSalesUnitQuantity: Number.isFinite(
|
|
626
|
+
defaultSalesUnitQuantity,
|
|
627
|
+
)
|
|
628
|
+
? defaultSalesUnitQuantity
|
|
629
|
+
: null,
|
|
388
630
|
} satisfies ProductOption,
|
|
389
|
-
} as LookupSelectItem & { option: ProductOption }
|
|
631
|
+
} as LookupSelectItem & { option: ProductOption };
|
|
390
632
|
})
|
|
391
|
-
.filter(
|
|
633
|
+
.filter(
|
|
634
|
+
(entry): entry is LookupSelectItem & { option: ProductOption } =>
|
|
635
|
+
Boolean(entry),
|
|
636
|
+
)
|
|
392
637
|
.map((entry) => {
|
|
393
|
-
productOptionsRef.current.set(entry.option.id, entry.option)
|
|
394
|
-
return entry
|
|
395
|
-
})
|
|
638
|
+
productOptionsRef.current.set(entry.option.id, entry.option);
|
|
639
|
+
return entry;
|
|
640
|
+
});
|
|
396
641
|
},
|
|
397
642
|
[],
|
|
398
|
-
)
|
|
643
|
+
);
|
|
399
644
|
|
|
400
645
|
const loadVariantOptions = React.useCallback(
|
|
401
|
-
async (
|
|
402
|
-
|
|
403
|
-
|
|
646
|
+
async (
|
|
647
|
+
productId: string,
|
|
648
|
+
fallbackThumbnail?: string | null,
|
|
649
|
+
): Promise<LookupSelectItem[]> => {
|
|
650
|
+
if (!productId) return [];
|
|
651
|
+
const response = await apiCall<{
|
|
652
|
+
items?: Array<Record<string, unknown>>;
|
|
653
|
+
}>(
|
|
404
654
|
`/api/catalog/variants?productId=${encodeURIComponent(productId)}&pageSize=50`,
|
|
405
655
|
undefined,
|
|
406
656
|
{ fallback: { items: [] } },
|
|
407
|
-
)
|
|
408
|
-
const items = Array.isArray(response.result?.items)
|
|
657
|
+
);
|
|
658
|
+
const items = Array.isArray(response.result?.items)
|
|
659
|
+
? response.result.items
|
|
660
|
+
: [];
|
|
409
661
|
return items
|
|
410
662
|
.map((item) => {
|
|
411
|
-
const id = typeof item.id ===
|
|
412
|
-
if (!id) return null
|
|
413
|
-
const title = typeof item.name ===
|
|
414
|
-
const
|
|
415
|
-
const
|
|
663
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
664
|
+
if (!id) return null;
|
|
665
|
+
const title = typeof item.name === "string" ? item.name : id;
|
|
666
|
+
const variantItem = item as ApiVariantItem;
|
|
667
|
+
const sku =
|
|
668
|
+
typeof variantItem.sku === "string" ? variantItem.sku : null;
|
|
669
|
+
const metadata =
|
|
670
|
+
typeof variantItem.metadata === "object" && variantItem.metadata
|
|
671
|
+
? variantItem.metadata
|
|
672
|
+
: null;
|
|
673
|
+
const variantMeta = metadata as ApiPricingMetadata | null;
|
|
416
674
|
const variantTaxRateId =
|
|
417
|
-
typeof
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
675
|
+
typeof variantMeta?.taxRateId === "string" &&
|
|
676
|
+
variantMeta.taxRateId.trim().length
|
|
677
|
+
? variantMeta.taxRateId.trim()
|
|
678
|
+
: typeof variantMeta?.tax_rate_id === "string" &&
|
|
679
|
+
variantMeta.tax_rate_id.trim().length
|
|
680
|
+
? variantMeta.tax_rate_id.trim()
|
|
681
|
+
: null;
|
|
422
682
|
const variantTaxRate = normalizeNumber(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
683
|
+
variantItem.tax_rate ??
|
|
684
|
+
variantItem.taxRate ??
|
|
685
|
+
variantMeta?.tax_rate ??
|
|
686
|
+
variantMeta?.taxRate,
|
|
687
|
+
Number.NaN,
|
|
688
|
+
);
|
|
426
689
|
const thumbnail =
|
|
427
|
-
typeof
|
|
428
|
-
?
|
|
429
|
-
: typeof
|
|
430
|
-
?
|
|
431
|
-
: fallbackThumbnail ?? null
|
|
690
|
+
typeof variantItem.default_media_url === "string"
|
|
691
|
+
? variantItem.default_media_url
|
|
692
|
+
: typeof variantItem.thumbnailUrl === "string"
|
|
693
|
+
? variantItem.thumbnailUrl
|
|
694
|
+
: (fallbackThumbnail ?? null);
|
|
432
695
|
return {
|
|
433
696
|
id,
|
|
434
697
|
title,
|
|
435
698
|
subtitle: sku ?? undefined,
|
|
436
|
-
icon: thumbnail
|
|
437
|
-
|
|
438
|
-
|
|
699
|
+
icon: thumbnail ? (
|
|
700
|
+
<img
|
|
701
|
+
src={thumbnail}
|
|
702
|
+
alt={title}
|
|
703
|
+
className="h-8 w-8 rounded object-cover"
|
|
704
|
+
/>
|
|
705
|
+
) : (
|
|
706
|
+
buildPlaceholder(title)
|
|
707
|
+
),
|
|
439
708
|
option: {
|
|
440
709
|
id,
|
|
441
710
|
title,
|
|
@@ -444,114 +713,240 @@ export function LineItemDialog({
|
|
|
444
713
|
taxRateId: variantTaxRateId,
|
|
445
714
|
taxRate: Number.isFinite(variantTaxRate) ? variantTaxRate : null,
|
|
446
715
|
} satisfies VariantOption,
|
|
447
|
-
} as LookupSelectItem & { option: VariantOption }
|
|
716
|
+
} as LookupSelectItem & { option: VariantOption };
|
|
448
717
|
})
|
|
449
|
-
.filter(
|
|
718
|
+
.filter(
|
|
719
|
+
(entry): entry is LookupSelectItem & { option: VariantOption } =>
|
|
720
|
+
Boolean(entry),
|
|
721
|
+
)
|
|
450
722
|
.map((entry) => {
|
|
451
|
-
variantOptionsRef.current.set(entry.option.id, entry.option)
|
|
452
|
-
return entry
|
|
453
|
-
})
|
|
723
|
+
variantOptionsRef.current.set(entry.option.id, entry.option);
|
|
724
|
+
return entry;
|
|
725
|
+
});
|
|
454
726
|
},
|
|
455
727
|
[],
|
|
456
|
-
)
|
|
728
|
+
);
|
|
457
729
|
|
|
458
|
-
const
|
|
459
|
-
async (
|
|
730
|
+
const loadProductUnits = React.useCallback(
|
|
731
|
+
async (
|
|
732
|
+
productId: string | null,
|
|
733
|
+
option: ProductOption | null,
|
|
734
|
+
): Promise<UnitOption[]> => {
|
|
460
735
|
if (!productId) {
|
|
461
|
-
|
|
462
|
-
return []
|
|
736
|
+
setUnitOptions([]);
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
const map = new Map<string, UnitOption>();
|
|
740
|
+
let baseUnit = normalizeUnitCode(option?.defaultUnit);
|
|
741
|
+
let defaultSalesUnit = normalizeUnitCode(option?.defaultSalesUnit);
|
|
742
|
+
if (!baseUnit || !defaultSalesUnit) {
|
|
743
|
+
try {
|
|
744
|
+
const response = await apiCall<{
|
|
745
|
+
items?: Array<Record<string, unknown>>;
|
|
746
|
+
}>(
|
|
747
|
+
`/api/catalog/products?id=${encodeURIComponent(productId)}&pageSize=1`,
|
|
748
|
+
undefined,
|
|
749
|
+
{ fallback: { items: [] } },
|
|
750
|
+
);
|
|
751
|
+
const records = Array.isArray(response.result?.items)
|
|
752
|
+
? response.result.items
|
|
753
|
+
: [];
|
|
754
|
+
const matched =
|
|
755
|
+
records.find((entry) => entry.id === productId) ??
|
|
756
|
+
records[0] ??
|
|
757
|
+
null;
|
|
758
|
+
if (matched) {
|
|
759
|
+
const matchedUom = getUomProductFields(matched);
|
|
760
|
+
baseUnit = baseUnit ?? matchedUom.defaultUnit;
|
|
761
|
+
defaultSalesUnit = defaultSalesUnit ?? matchedUom.defaultSalesUnit;
|
|
762
|
+
}
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.error("sales.document.items.loadProductUnits.hydration", err);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (baseUnit) {
|
|
768
|
+
map.set(baseUnit, { code: baseUnit, toBaseFactor: 1, isBase: true });
|
|
463
769
|
}
|
|
464
|
-
setPriceLoading(true)
|
|
465
770
|
try {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`/api/catalog/
|
|
771
|
+
const response = await apiCall<{
|
|
772
|
+
items?: Array<Record<string, unknown>>;
|
|
773
|
+
}>(
|
|
774
|
+
`/api/catalog/product-unit-conversions?productId=${encodeURIComponent(productId)}&pageSize=100`,
|
|
470
775
|
undefined,
|
|
471
776
|
{ fallback: { items: [] } },
|
|
472
|
-
)
|
|
473
|
-
const
|
|
777
|
+
);
|
|
778
|
+
const rows = Array.isArray(response.result?.items)
|
|
779
|
+
? response.result.items
|
|
780
|
+
: [];
|
|
781
|
+
for (const row of rows) {
|
|
782
|
+
const conv = getUomConversionFields(row);
|
|
783
|
+
if (!conv.unitCode) continue;
|
|
784
|
+
if (!conv.isActive) continue;
|
|
785
|
+
map.set(conv.unitCode, {
|
|
786
|
+
code: conv.unitCode,
|
|
787
|
+
toBaseFactor:
|
|
788
|
+
Number.isFinite(conv.toBaseFactor) && conv.toBaseFactor > 0
|
|
789
|
+
? conv.toBaseFactor
|
|
790
|
+
: null,
|
|
791
|
+
isBase: baseUnit ? conv.unitCode === baseUnit : false,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
} catch (err) {
|
|
795
|
+
console.error("sales.document.items.loadUnits", err);
|
|
796
|
+
}
|
|
797
|
+
if (defaultSalesUnit && !map.has(defaultSalesUnit)) {
|
|
798
|
+
map.set(defaultSalesUnit, {
|
|
799
|
+
code: defaultSalesUnit,
|
|
800
|
+
toBaseFactor: baseUnit && defaultSalesUnit === baseUnit ? 1 : null,
|
|
801
|
+
isBase: baseUnit ? defaultSalesUnit === baseUnit : false,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const nextOptions = Array.from(map.values()).sort((left, right) =>
|
|
805
|
+
left.code.localeCompare(right.code),
|
|
806
|
+
);
|
|
807
|
+
setUnitOptions(nextOptions);
|
|
808
|
+
return nextOptions;
|
|
809
|
+
},
|
|
810
|
+
[],
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const loadPrices = React.useCallback(
|
|
814
|
+
async (
|
|
815
|
+
productId: string | null,
|
|
816
|
+
variantId: string | null,
|
|
817
|
+
quantity?: string | number | null,
|
|
818
|
+
quantityUnit?: string | null,
|
|
819
|
+
) => {
|
|
820
|
+
if (!productId) {
|
|
821
|
+
setPriceOptions([]);
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
setPriceLoading(true);
|
|
825
|
+
try {
|
|
826
|
+
const params = new URLSearchParams({ productId, pageSize: "20" });
|
|
827
|
+
if (variantId) params.set("variantId", variantId);
|
|
828
|
+
const quantityValue = normalizeNumber(quantity, Number.NaN);
|
|
829
|
+
if (Number.isFinite(quantityValue) && quantityValue > 0) {
|
|
830
|
+
params.set("quantity", String(quantityValue));
|
|
831
|
+
}
|
|
832
|
+
const quantityUnitCode = normalizeUnitCode(quantityUnit);
|
|
833
|
+
if (quantityUnitCode) {
|
|
834
|
+
params.set("quantityUnit", quantityUnitCode);
|
|
835
|
+
}
|
|
836
|
+
const response = await apiCall<{
|
|
837
|
+
items?: Array<Record<string, unknown>>;
|
|
838
|
+
}>(`/api/catalog/prices?${params.toString()}`, undefined, {
|
|
839
|
+
fallback: { items: [] },
|
|
840
|
+
});
|
|
841
|
+
const items = Array.isArray(response.result?.items)
|
|
842
|
+
? response.result.items
|
|
843
|
+
: [];
|
|
474
844
|
const mapped: PriceOption[] = items
|
|
475
845
|
.map((item) => {
|
|
476
|
-
const id = typeof item.id ===
|
|
477
|
-
if (!id) return null
|
|
478
|
-
const
|
|
479
|
-
|
|
846
|
+
const id = typeof item.id === "string" ? item.id : null;
|
|
847
|
+
if (!id) return null;
|
|
848
|
+
const amountNetRaw = normalizeNumber(
|
|
849
|
+
(item as ApiPriceItem).unit_price_net,
|
|
850
|
+
Number.NaN,
|
|
851
|
+
);
|
|
852
|
+
const amountGrossRaw = normalizeNumber(
|
|
853
|
+
(item as ApiPriceItem).unit_price_gross,
|
|
854
|
+
Number.NaN,
|
|
855
|
+
);
|
|
856
|
+
const amountNet = Number.isFinite(amountNetRaw)
|
|
857
|
+
? amountNetRaw
|
|
858
|
+
: null;
|
|
859
|
+
const amountGross = Number.isFinite(amountGrossRaw)
|
|
860
|
+
? amountGrossRaw
|
|
861
|
+
: null;
|
|
480
862
|
const currency =
|
|
481
|
-
typeof (item as
|
|
482
|
-
? (item as
|
|
483
|
-
: typeof (item as
|
|
484
|
-
? (item as
|
|
485
|
-
: null
|
|
863
|
+
typeof (item as ApiPriceItem).currency_code === "string"
|
|
864
|
+
? (item as ApiPriceItem).currency_code
|
|
865
|
+
: typeof (item as ApiPriceItem).currencyCode === "string"
|
|
866
|
+
? (item as ApiPriceItem).currencyCode
|
|
867
|
+
: null;
|
|
486
868
|
const displayMode =
|
|
487
|
-
(item as
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
869
|
+
(item as ApiPriceItem).display_mode === "including-tax" ||
|
|
870
|
+
(item as ApiPriceItem).display_mode === "excluding-tax"
|
|
871
|
+
? (item as ApiPriceItem).display_mode
|
|
872
|
+
: (item as ApiPriceItem).displayMode === "including-tax" ||
|
|
873
|
+
(item as ApiPriceItem).displayMode === "excluding-tax"
|
|
874
|
+
? (item as ApiPriceItem).displayMode
|
|
875
|
+
: null;
|
|
876
|
+
const taxRateRaw = normalizeNumber(
|
|
877
|
+
(item as ApiPriceItem).tax_rate,
|
|
878
|
+
Number.NaN,
|
|
879
|
+
);
|
|
880
|
+
const taxRate = Number.isFinite(taxRateRaw) ? taxRateRaw : null;
|
|
493
881
|
const priceKindId =
|
|
494
|
-
typeof (item as
|
|
495
|
-
? (item as
|
|
496
|
-
: typeof (item as
|
|
497
|
-
? (item as
|
|
498
|
-
: null
|
|
882
|
+
typeof (item as ApiPriceItem).price_kind_id === "string"
|
|
883
|
+
? (item as ApiPriceItem).price_kind_id
|
|
884
|
+
: typeof (item as ApiPriceItem).priceKindId === "string"
|
|
885
|
+
? (item as ApiPriceItem).priceKindId
|
|
886
|
+
: null;
|
|
499
887
|
const priceKindTitle =
|
|
500
|
-
typeof (item as
|
|
501
|
-
? (item as
|
|
502
|
-
: typeof (item as
|
|
503
|
-
? (item as
|
|
504
|
-
: typeof (item as
|
|
888
|
+
typeof (item as ApiPriceItem).price_kind_title === "string"
|
|
889
|
+
? (item as ApiPriceItem).price_kind_title
|
|
890
|
+
: typeof (item as ApiPriceItem).priceKindTitle === "string"
|
|
891
|
+
? (item as ApiPriceItem).priceKindTitle
|
|
892
|
+
: typeof (item as ApiPriceItem).price_kind === "object" &&
|
|
505
893
|
item &&
|
|
506
|
-
typeof (item as
|
|
507
|
-
? (item as
|
|
508
|
-
: typeof (item as
|
|
894
|
+
typeof (item as ApiPriceItem).price_kind?.title === "string"
|
|
895
|
+
? (item as ApiPriceItem).price_kind!.title
|
|
896
|
+
: typeof (item as ApiPriceItem).price_kind === "object" &&
|
|
509
897
|
item &&
|
|
510
|
-
typeof (item as
|
|
511
|
-
? (item as
|
|
512
|
-
: null
|
|
898
|
+
typeof (item as ApiPriceItem).price_kind?.name === "string"
|
|
899
|
+
? (item as ApiPriceItem).price_kind!.name
|
|
900
|
+
: null;
|
|
513
901
|
const priceKindCode =
|
|
514
|
-
typeof (item as
|
|
515
|
-
? (item as
|
|
516
|
-
: typeof (item as
|
|
517
|
-
? (item as
|
|
518
|
-
: typeof (item as
|
|
902
|
+
typeof (item as ApiPriceItem).price_kind_code === "string"
|
|
903
|
+
? (item as ApiPriceItem).price_kind_code
|
|
904
|
+
: typeof (item as ApiPriceItem).priceKindCode === "string"
|
|
905
|
+
? (item as ApiPriceItem).priceKindCode
|
|
906
|
+
: typeof (item as ApiPriceItem).price_kind === "object" &&
|
|
519
907
|
item &&
|
|
520
|
-
typeof (item as
|
|
521
|
-
? (item as
|
|
522
|
-
: null
|
|
908
|
+
typeof (item as ApiPriceItem).price_kind?.code === "string"
|
|
909
|
+
? (item as ApiPriceItem).price_kind!.code
|
|
910
|
+
: null;
|
|
523
911
|
const resolvedPriceKindTitle =
|
|
524
912
|
priceKindTitle ??
|
|
525
913
|
priceKindCode ??
|
|
526
|
-
(typeof (item as
|
|
914
|
+
(typeof (item as ApiPriceItem).kind === "string"
|
|
915
|
+
? (item as ApiPriceItem).kind
|
|
916
|
+
: null);
|
|
527
917
|
const labelParts = [
|
|
528
|
-
displayMode ===
|
|
918
|
+
displayMode === "including-tax" &&
|
|
919
|
+
amountGross !== null &&
|
|
920
|
+
currency
|
|
529
921
|
? formatMoney(amountGross, currency)
|
|
530
922
|
: null,
|
|
531
|
-
displayMode ===
|
|
923
|
+
displayMode === "excluding-tax" && amountNet !== null && currency
|
|
532
924
|
? formatMoney(amountNet, currency)
|
|
533
925
|
: null,
|
|
534
926
|
displayMode
|
|
535
|
-
? displayMode ===
|
|
536
|
-
? t(
|
|
537
|
-
: t(
|
|
927
|
+
? displayMode === "including-tax"
|
|
928
|
+
? t("sales.documents.items.priceGross", "Gross")
|
|
929
|
+
: t("sales.documents.items.priceNet", "Net")
|
|
538
930
|
: null,
|
|
539
|
-
].filter(Boolean)
|
|
540
|
-
const { reason, tags } = buildPriceScopeReason(
|
|
931
|
+
].filter(Boolean);
|
|
932
|
+
const { reason, tags } = buildPriceScopeReason(
|
|
933
|
+
item,
|
|
934
|
+
(key, fallback) => t(key, fallback),
|
|
935
|
+
);
|
|
541
936
|
const label =
|
|
542
937
|
labelParts.length > 0
|
|
543
|
-
? labelParts.join(
|
|
938
|
+
? labelParts.join(" • ")
|
|
544
939
|
: amountGross !== null && currency
|
|
545
940
|
? formatMoney(amountGross, currency)
|
|
546
941
|
: amountNet !== null && currency
|
|
547
942
|
? formatMoney(amountNet, currency)
|
|
548
|
-
: id
|
|
943
|
+
: id;
|
|
549
944
|
return {
|
|
550
945
|
id,
|
|
551
946
|
amountNet: amountNet ?? null,
|
|
552
947
|
amountGross: amountGross ?? null,
|
|
553
948
|
currencyCode: currency,
|
|
554
|
-
displayMode: displayMode as PriceOption[
|
|
949
|
+
displayMode: displayMode as PriceOption["displayMode"],
|
|
555
950
|
taxRate: Number.isFinite(taxRate) ? taxRate : null,
|
|
556
951
|
label,
|
|
557
952
|
priceKindId,
|
|
@@ -559,190 +954,383 @@ export function LineItemDialog({
|
|
|
559
954
|
priceKindCode: priceKindCode ?? null,
|
|
560
955
|
scopeReason: reason,
|
|
561
956
|
scopeTags: tags,
|
|
562
|
-
} as PriceOption
|
|
957
|
+
} as PriceOption;
|
|
563
958
|
})
|
|
564
|
-
.filter((entry): entry is PriceOption => Boolean(entry))
|
|
565
|
-
setPriceOptions(mapped)
|
|
566
|
-
return mapped
|
|
959
|
+
.filter((entry): entry is PriceOption => Boolean(entry));
|
|
960
|
+
setPriceOptions(mapped);
|
|
961
|
+
return mapped;
|
|
567
962
|
} catch (err) {
|
|
568
|
-
console.error(
|
|
569
|
-
return []
|
|
963
|
+
console.error("sales.document.items.loadPrices", err);
|
|
964
|
+
return [];
|
|
570
965
|
} finally {
|
|
571
|
-
setPriceLoading(false)
|
|
966
|
+
setPriceLoading(false);
|
|
572
967
|
}
|
|
573
968
|
},
|
|
574
969
|
[t],
|
|
575
|
-
)
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const selectPriceAfterRefresh = React.useCallback(
|
|
973
|
+
(
|
|
974
|
+
prices: PriceOption[],
|
|
975
|
+
currentPriceId: string | null,
|
|
976
|
+
currentPriceKindId: string | null,
|
|
977
|
+
): PriceOption | null => {
|
|
978
|
+
if (!prices.length) return null;
|
|
979
|
+
if (currentPriceId) {
|
|
980
|
+
const sameId = prices.find((entry) => entry.id === currentPriceId);
|
|
981
|
+
if (sameId) return sameId;
|
|
982
|
+
}
|
|
983
|
+
if (currentPriceKindId) {
|
|
984
|
+
const sameKind = prices.find(
|
|
985
|
+
(entry) => entry.priceKindId === currentPriceKindId,
|
|
986
|
+
);
|
|
987
|
+
if (sameKind) return sameKind;
|
|
988
|
+
}
|
|
989
|
+
return prices[0] ?? null;
|
|
990
|
+
},
|
|
991
|
+
[],
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
const resolveUnitPriceFactor = React.useCallback(
|
|
995
|
+
(quantityUnit: string | null | undefined): number => {
|
|
996
|
+
const normalized = normalizeUnitCode(quantityUnit);
|
|
997
|
+
if (!normalized) return 1;
|
|
998
|
+
const unit = unitOptions.find((entry) => entry.code === normalized) ?? null;
|
|
999
|
+
const factor = normalizeNumber(unit?.toBaseFactor, Number.NaN);
|
|
1000
|
+
if (!Number.isFinite(factor) || factor <= 0) return 1;
|
|
1001
|
+
return factor;
|
|
1002
|
+
},
|
|
1003
|
+
[unitOptions],
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
const convertUnitPriceForUnitChange = React.useCallback(
|
|
1007
|
+
(
|
|
1008
|
+
rawUnitPrice: unknown,
|
|
1009
|
+
fromUnit: string | null | undefined,
|
|
1010
|
+
toUnit: string | null | undefined,
|
|
1011
|
+
): string | null => {
|
|
1012
|
+
const amount = normalizeNumber(rawUnitPrice, Number.NaN);
|
|
1013
|
+
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
1014
|
+
const fromCode = normalizeUnitCode(fromUnit);
|
|
1015
|
+
const toCode = normalizeUnitCode(toUnit);
|
|
1016
|
+
if (!fromCode || !toCode || fromCode === toCode) return null;
|
|
1017
|
+
const fromFactor = resolveUnitPriceFactor(fromCode);
|
|
1018
|
+
const toFactor = resolveUnitPriceFactor(toCode);
|
|
1019
|
+
if (
|
|
1020
|
+
!Number.isFinite(fromFactor) ||
|
|
1021
|
+
!Number.isFinite(toFactor) ||
|
|
1022
|
+
fromFactor <= 0 ||
|
|
1023
|
+
toFactor <= 0
|
|
1024
|
+
) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
const baseAmount = amount / fromFactor;
|
|
1028
|
+
const convertedAmount = baseAmount * toFactor;
|
|
1029
|
+
if (!Number.isFinite(convertedAmount) || convertedAmount <= 0) return null;
|
|
1030
|
+
return normalizeUnitPriceInputValue(convertedAmount);
|
|
1031
|
+
},
|
|
1032
|
+
[resolveUnitPriceFactor],
|
|
1033
|
+
);
|
|
576
1034
|
|
|
577
|
-
const
|
|
578
|
-
|
|
1035
|
+
const applyPriceSelection = React.useCallback(
|
|
1036
|
+
(
|
|
1037
|
+
selected: PriceOption | null,
|
|
1038
|
+
setFormValue: ((id: string, value: unknown) => void) | undefined,
|
|
1039
|
+
options?: {
|
|
1040
|
+
fallbackTaxSource?: { taxRateId?: string | null; taxRate?: number | null } | null;
|
|
1041
|
+
quantityUnit?: string | null;
|
|
1042
|
+
},
|
|
1043
|
+
) => {
|
|
1044
|
+
if (!setFormValue) return;
|
|
1045
|
+
if (selected) {
|
|
1046
|
+
const mode =
|
|
1047
|
+
selected.displayMode === "excluding-tax" ? "net" : "gross";
|
|
1048
|
+
const amountPerBaseUnit =
|
|
1049
|
+
mode === "net"
|
|
1050
|
+
? (selected.amountNet ?? selected.amountGross ?? 0)
|
|
1051
|
+
: (selected.amountGross ?? selected.amountNet ?? 0);
|
|
1052
|
+
const factor = resolveUnitPriceFactor(options?.quantityUnit ?? null);
|
|
1053
|
+
const amount = Number.isFinite(amountPerBaseUnit * factor)
|
|
1054
|
+
? amountPerBaseUnit * factor
|
|
1055
|
+
: amountPerBaseUnit;
|
|
1056
|
+
setFormValue("priceId", selected.id);
|
|
1057
|
+
setFormValue("priceMode", mode);
|
|
1058
|
+
setFormValue("unitPrice", normalizeUnitPriceInputValue(amount));
|
|
1059
|
+
setFormValue("taxRate", selected.taxRate ?? null);
|
|
1060
|
+
setFormValue("taxRateId", findTaxRateIdByValue(selected.taxRate));
|
|
1061
|
+
setFormValue(
|
|
1062
|
+
"currencyCode",
|
|
1063
|
+
selected.currencyCode ?? currencyCode ?? null,
|
|
1064
|
+
);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const fallbackTax = resolveTaxSelection(options?.fallbackTaxSource ?? null);
|
|
1068
|
+
setFormValue("taxRate", fallbackTax.taxRate ?? null);
|
|
1069
|
+
setFormValue("taxRateId", fallbackTax.taxRateId ?? null);
|
|
1070
|
+
},
|
|
1071
|
+
[currencyCode, findTaxRateIdByValue, resolveTaxSelection, resolveUnitPriceFactor],
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
const loadLineStatuses = React.useCallback(async (): Promise<
|
|
1075
|
+
StatusOption[]
|
|
1076
|
+
> => {
|
|
1077
|
+
setLineStatusLoading(true);
|
|
579
1078
|
try {
|
|
580
|
-
const params = new URLSearchParams({ page:
|
|
581
|
-
const response = await apiCall<{
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
)
|
|
586
|
-
const items = Array.isArray(response.result?.items)
|
|
1079
|
+
const params = new URLSearchParams({ page: "1", pageSize: "100" });
|
|
1080
|
+
const response = await apiCall<{
|
|
1081
|
+
items?: Array<Record<string, unknown>>;
|
|
1082
|
+
}>(`/api/sales/order-line-statuses?${params.toString()}`, undefined, {
|
|
1083
|
+
fallback: { items: [] },
|
|
1084
|
+
});
|
|
1085
|
+
const items = Array.isArray(response.result?.items)
|
|
1086
|
+
? response.result.items
|
|
1087
|
+
: [];
|
|
587
1088
|
const mapped = items
|
|
588
1089
|
.map((entry) => {
|
|
589
|
-
const id = typeof entry.id ===
|
|
590
|
-
const value = typeof entry.value ===
|
|
591
|
-
if (!id || !value) return null
|
|
1090
|
+
const id = typeof entry.id === "string" ? entry.id : null;
|
|
1091
|
+
const value = typeof entry.value === "string" ? entry.value : null;
|
|
1092
|
+
if (!id || !value) return null;
|
|
592
1093
|
const label =
|
|
593
|
-
typeof entry.label ===
|
|
1094
|
+
typeof entry.label === "string" && entry.label.trim().length
|
|
594
1095
|
? entry.label
|
|
595
|
-
: value
|
|
1096
|
+
: value;
|
|
596
1097
|
const color =
|
|
597
|
-
typeof entry.color ===
|
|
1098
|
+
typeof entry.color === "string" && entry.color.trim().length
|
|
1099
|
+
? entry.color
|
|
1100
|
+
: null;
|
|
598
1101
|
const icon =
|
|
599
|
-
typeof entry.icon ===
|
|
600
|
-
|
|
1102
|
+
typeof entry.icon === "string" && entry.icon.trim().length
|
|
1103
|
+
? entry.icon
|
|
1104
|
+
: null;
|
|
1105
|
+
return { id, value, label, color, icon };
|
|
601
1106
|
})
|
|
602
|
-
.filter((entry): entry is StatusOption => Boolean(entry))
|
|
603
|
-
setLineStatuses(mapped)
|
|
604
|
-
return mapped
|
|
1107
|
+
.filter((entry): entry is StatusOption => Boolean(entry));
|
|
1108
|
+
setLineStatuses(mapped);
|
|
1109
|
+
return mapped;
|
|
605
1110
|
} catch (err) {
|
|
606
|
-
console.error(
|
|
607
|
-
setLineStatuses([])
|
|
608
|
-
return []
|
|
1111
|
+
console.error("sales.lines.statuses.load", err);
|
|
1112
|
+
setLineStatuses([]);
|
|
1113
|
+
return [];
|
|
609
1114
|
} finally {
|
|
610
|
-
setLineStatusLoading(false)
|
|
1115
|
+
setLineStatusLoading(false);
|
|
611
1116
|
}
|
|
612
|
-
}, [])
|
|
1117
|
+
}, []);
|
|
613
1118
|
|
|
614
1119
|
const fetchLineStatusItems = React.useCallback(
|
|
615
1120
|
async (query?: string): Promise<LookupSelectItem[]> => {
|
|
616
1121
|
const options =
|
|
617
|
-
lineStatuses.length && !query ? lineStatuses : await loadLineStatuses()
|
|
618
|
-
const term = query?.trim().toLowerCase() ??
|
|
619
|
-
const currentMap = options.reduce<
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1122
|
+
lineStatuses.length && !query ? lineStatuses : await loadLineStatuses();
|
|
1123
|
+
const term = query?.trim().toLowerCase() ?? "";
|
|
1124
|
+
const currentMap = options.reduce<
|
|
1125
|
+
Record<
|
|
1126
|
+
string,
|
|
1127
|
+
{
|
|
1128
|
+
value: string;
|
|
1129
|
+
label: string;
|
|
1130
|
+
color?: string | null;
|
|
1131
|
+
icon?: string | null;
|
|
626
1132
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
{
|
|
630
|
-
|
|
1133
|
+
>
|
|
1134
|
+
>((acc, entry) => {
|
|
1135
|
+
acc[entry.value] = {
|
|
1136
|
+
value: entry.value,
|
|
1137
|
+
label: entry.label,
|
|
1138
|
+
color: entry.color,
|
|
1139
|
+
icon: entry.icon ?? null,
|
|
1140
|
+
};
|
|
1141
|
+
return acc;
|
|
1142
|
+
}, {});
|
|
631
1143
|
return options
|
|
632
1144
|
.filter(
|
|
633
1145
|
(option) =>
|
|
634
1146
|
!term.length ||
|
|
635
1147
|
option.label.toLowerCase().includes(term) ||
|
|
636
|
-
option.value.toLowerCase().includes(term)
|
|
1148
|
+
option.value.toLowerCase().includes(term),
|
|
637
1149
|
)
|
|
638
1150
|
.map<LookupSelectItem>((option) => ({
|
|
639
1151
|
id: option.id,
|
|
640
1152
|
title: option.label,
|
|
641
1153
|
subtitle: option.label !== option.value ? option.value : undefined,
|
|
642
|
-
icon:
|
|
643
|
-
|
|
1154
|
+
icon:
|
|
1155
|
+
renderDictionaryIcon(option.icon, "h-4 w-4") ??
|
|
1156
|
+
renderDictionaryColor(option.color, "h-4 w-4 rounded-full"),
|
|
1157
|
+
}));
|
|
644
1158
|
},
|
|
645
1159
|
[lineStatuses, loadLineStatuses],
|
|
646
|
-
)
|
|
1160
|
+
);
|
|
647
1161
|
|
|
648
1162
|
React.useEffect(() => {
|
|
649
|
-
if (!open) return
|
|
650
|
-
loadTaxRates().catch(() => {})
|
|
651
|
-
loadLineStatuses().catch(() => {})
|
|
652
|
-
}, [loadLineStatuses, loadTaxRates, open])
|
|
1163
|
+
if (!open) return;
|
|
1164
|
+
loadTaxRates().catch(() => {});
|
|
1165
|
+
loadLineStatuses().catch(() => {});
|
|
1166
|
+
}, [loadLineStatuses, loadTaxRates, open]);
|
|
653
1167
|
|
|
654
1168
|
const handleFormSubmit = React.useCallback(
|
|
655
1169
|
async (values: LineFormState & Record<string, unknown>) => {
|
|
656
|
-
console.groupCollapsed('sales.line.submit.start')
|
|
657
|
-
console.log('raw values', values)
|
|
658
1170
|
// Resolve required scope and ids
|
|
659
|
-
const resolvedDocumentId =
|
|
660
|
-
|
|
661
|
-
|
|
1171
|
+
const resolvedDocumentId =
|
|
1172
|
+
typeof documentId === "string" && documentId.trim().length
|
|
1173
|
+
? documentId
|
|
1174
|
+
: null;
|
|
1175
|
+
const resolvedOrg = resolvedOrganizationId;
|
|
1176
|
+
const resolvedTenant = resolvedTenantId;
|
|
662
1177
|
|
|
663
1178
|
if (!resolvedOrg || !resolvedTenant || !resolvedDocumentId) {
|
|
664
1179
|
throw createCrudFormError(
|
|
665
|
-
t(
|
|
666
|
-
|
|
1180
|
+
t(
|
|
1181
|
+
"sales.documents.items.errorScope",
|
|
1182
|
+
"Organization and tenant are required.",
|
|
1183
|
+
),
|
|
1184
|
+
);
|
|
667
1185
|
}
|
|
668
|
-
const lineMode = values.lineMode ===
|
|
669
|
-
const isCustomLine = lineMode ===
|
|
1186
|
+
const lineMode = values.lineMode === "custom" ? "custom" : "catalog";
|
|
1187
|
+
const isCustomLine = lineMode === "custom";
|
|
670
1188
|
|
|
671
1189
|
if (!isCustomLine && !values.productId) {
|
|
672
1190
|
throw createCrudFormError(
|
|
673
|
-
t(
|
|
674
|
-
|
|
675
|
-
|
|
1191
|
+
t(
|
|
1192
|
+
"sales.documents.items.errorProductRequired",
|
|
1193
|
+
"Select a product to continue.",
|
|
1194
|
+
),
|
|
1195
|
+
{
|
|
1196
|
+
productId: t(
|
|
1197
|
+
"sales.documents.items.errorProductRequired",
|
|
1198
|
+
"Select a product to continue.",
|
|
1199
|
+
),
|
|
1200
|
+
},
|
|
1201
|
+
);
|
|
676
1202
|
}
|
|
677
1203
|
if (!isCustomLine && !values.variantId) {
|
|
678
1204
|
throw createCrudFormError(
|
|
679
|
-
t(
|
|
680
|
-
|
|
681
|
-
|
|
1205
|
+
t(
|
|
1206
|
+
"sales.documents.items.errorVariantRequired",
|
|
1207
|
+
"Select a variant to continue.",
|
|
1208
|
+
),
|
|
1209
|
+
{
|
|
1210
|
+
variantId: t(
|
|
1211
|
+
"sales.documents.items.errorVariantRequired",
|
|
1212
|
+
"Select a variant to continue.",
|
|
1213
|
+
),
|
|
1214
|
+
},
|
|
1215
|
+
);
|
|
682
1216
|
}
|
|
683
1217
|
|
|
684
|
-
const qtyNumber = Number(values.quantity ??
|
|
685
|
-
console.log('quantity raw -> parsed', { raw: values.quantity, parsed: qtyNumber })
|
|
1218
|
+
const qtyNumber = Number(values.quantity ?? 0);
|
|
686
1219
|
if (!Number.isFinite(qtyNumber) || qtyNumber <= 0) {
|
|
687
1220
|
throw createCrudFormError(
|
|
688
|
-
t(
|
|
689
|
-
|
|
690
|
-
|
|
1221
|
+
t(
|
|
1222
|
+
"sales.documents.items.errorQuantity",
|
|
1223
|
+
"Quantity must be greater than 0.",
|
|
1224
|
+
),
|
|
1225
|
+
{
|
|
1226
|
+
quantity: t(
|
|
1227
|
+
"sales.documents.items.errorQuantity",
|
|
1228
|
+
"Quantity must be greater than 0.",
|
|
1229
|
+
),
|
|
1230
|
+
},
|
|
1231
|
+
);
|
|
691
1232
|
}
|
|
1233
|
+
const resolvedQuantityUnit = (() => {
|
|
1234
|
+
const entered = normalizeUnitCode(values.quantityUnit);
|
|
1235
|
+
if (isCustomLine) return entered;
|
|
1236
|
+
return (
|
|
1237
|
+
entered ??
|
|
1238
|
+
normalizeUnitCode(productOption?.defaultSalesUnit) ??
|
|
1239
|
+
normalizeUnitCode(productOption?.defaultUnit)
|
|
1240
|
+
);
|
|
1241
|
+
})();
|
|
692
1242
|
|
|
693
|
-
const unitPriceNumber = Number(values.unitPrice ??
|
|
694
|
-
console.log('unit price raw -> parsed', { raw: values.unitPrice, parsed: unitPriceNumber })
|
|
1243
|
+
const unitPriceNumber = Number(values.unitPrice ?? 0);
|
|
695
1244
|
if (!Number.isFinite(unitPriceNumber) || unitPriceNumber <= 0) {
|
|
696
1245
|
throw createCrudFormError(
|
|
697
|
-
t(
|
|
698
|
-
|
|
699
|
-
|
|
1246
|
+
t(
|
|
1247
|
+
"sales.documents.items.errorUnitPrice",
|
|
1248
|
+
"Unit price must be greater than 0.",
|
|
1249
|
+
),
|
|
1250
|
+
{
|
|
1251
|
+
unitPrice: t(
|
|
1252
|
+
"sales.documents.items.errorUnitPrice",
|
|
1253
|
+
"Unit price must be greater than 0.",
|
|
1254
|
+
),
|
|
1255
|
+
},
|
|
1256
|
+
);
|
|
700
1257
|
}
|
|
701
1258
|
|
|
702
|
-
const selectedPrice =
|
|
703
|
-
|
|
704
|
-
|
|
1259
|
+
const selectedPrice =
|
|
1260
|
+
!isCustomLine && values.priceId
|
|
1261
|
+
? (priceOptions.find((price) => price.id === values.priceId) ?? null)
|
|
1262
|
+
: null;
|
|
705
1263
|
const resolvedCurrency =
|
|
706
1264
|
(values.currencyCode as string | null | undefined) ??
|
|
707
1265
|
selectedPrice?.currencyCode ??
|
|
708
1266
|
currencyCode ??
|
|
709
|
-
null
|
|
1267
|
+
null;
|
|
710
1268
|
if (!resolvedCurrency) {
|
|
711
1269
|
throw createCrudFormError(
|
|
712
|
-
t(
|
|
713
|
-
{
|
|
714
|
-
|
|
1270
|
+
t("sales.documents.items.errorCurrency", "Currency is required."),
|
|
1271
|
+
{
|
|
1272
|
+
priceId: t(
|
|
1273
|
+
"sales.documents.items.errorCurrency",
|
|
1274
|
+
"Currency is required.",
|
|
1275
|
+
),
|
|
1276
|
+
},
|
|
1277
|
+
);
|
|
715
1278
|
}
|
|
716
1279
|
|
|
717
|
-
const resolvedNameRaw = (values.name ??
|
|
1280
|
+
const resolvedNameRaw = (values.name ?? "").toString().trim();
|
|
718
1281
|
const resolvedName = isCustomLine
|
|
719
1282
|
? resolvedNameRaw
|
|
720
|
-
: resolvedNameRaw ||
|
|
1283
|
+
: resolvedNameRaw ||
|
|
1284
|
+
variantOption?.title ||
|
|
1285
|
+
productOption?.title ||
|
|
1286
|
+
undefined;
|
|
721
1287
|
if (isCustomLine && !resolvedName) {
|
|
722
1288
|
throw createCrudFormError(
|
|
723
|
-
t(
|
|
724
|
-
|
|
725
|
-
|
|
1289
|
+
t(
|
|
1290
|
+
"sales.documents.items.errorNameRequired",
|
|
1291
|
+
"Name is required for custom lines.",
|
|
1292
|
+
),
|
|
1293
|
+
{
|
|
1294
|
+
name: t(
|
|
1295
|
+
"sales.documents.items.errorNameRequired",
|
|
1296
|
+
"Name is required for custom lines.",
|
|
1297
|
+
),
|
|
1298
|
+
},
|
|
1299
|
+
);
|
|
726
1300
|
}
|
|
727
|
-
const resolvedPriceMode = values.priceMode ===
|
|
1301
|
+
const resolvedPriceMode = values.priceMode === "net" ? "net" : "gross";
|
|
728
1302
|
const catalogSnapshot =
|
|
729
|
-
!isCustomLine &&
|
|
1303
|
+
!isCustomLine &&
|
|
1304
|
+
typeof values.catalogSnapshot === "object" &&
|
|
1305
|
+
values.catalogSnapshot
|
|
1306
|
+
? values.catalogSnapshot
|
|
1307
|
+
: null;
|
|
730
1308
|
const selectedTaxRateId =
|
|
731
|
-
typeof values.taxRateId ===
|
|
1309
|
+
typeof values.taxRateId === "string" && values.taxRateId.trim().length
|
|
732
1310
|
? values.taxRateId
|
|
733
|
-
: null
|
|
1311
|
+
: null;
|
|
734
1312
|
const resolvedTaxRate = Number.isFinite(values.taxRate)
|
|
735
1313
|
? (values.taxRate as number)
|
|
736
|
-
: normalizeNumber(values.taxRate)
|
|
737
|
-
const normalizedTaxRate = Number.isFinite(resolvedTaxRate)
|
|
1314
|
+
: normalizeNumber(values.taxRate);
|
|
1315
|
+
const normalizedTaxRate = Number.isFinite(resolvedTaxRate)
|
|
1316
|
+
? resolvedTaxRate
|
|
1317
|
+
: 0;
|
|
738
1318
|
const unitPriceNetValue =
|
|
739
|
-
resolvedPriceMode ===
|
|
1319
|
+
resolvedPriceMode === "net"
|
|
1320
|
+
? unitPriceNumber
|
|
1321
|
+
: unitPriceNumber / (1 + normalizedTaxRate / 100);
|
|
740
1322
|
const unitPriceGrossValue =
|
|
741
|
-
resolvedPriceMode ===
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
1323
|
+
resolvedPriceMode === "gross"
|
|
1324
|
+
? unitPriceNumber
|
|
1325
|
+
: unitPriceNumber * (1 + normalizedTaxRate / 100);
|
|
1326
|
+
const safeUnitPriceNet = Number.isFinite(unitPriceNetValue)
|
|
1327
|
+
? unitPriceNetValue
|
|
1328
|
+
: unitPriceNumber;
|
|
1329
|
+
const safeUnitPriceGross = Number.isFinite(unitPriceGrossValue)
|
|
1330
|
+
? unitPriceGrossValue
|
|
1331
|
+
: unitPriceNumber;
|
|
1332
|
+
const totalNetAmount = safeUnitPriceNet * qtyNumber;
|
|
1333
|
+
const totalGrossAmount = safeUnitPriceGross * qtyNumber;
|
|
746
1334
|
|
|
747
1335
|
const metadata = {
|
|
748
1336
|
...(catalogSnapshot ?? {}),
|
|
@@ -760,22 +1348,36 @@ export function LineItemDialog({
|
|
|
760
1348
|
? {
|
|
761
1349
|
variantTitle: variantOption.title,
|
|
762
1350
|
variantSku: variantOption.sku ?? null,
|
|
763
|
-
variantThumbnail:
|
|
1351
|
+
variantThumbnail:
|
|
1352
|
+
variantOption.thumbnailUrl ??
|
|
1353
|
+
productOption?.thumbnailUrl ??
|
|
1354
|
+
null,
|
|
764
1355
|
}
|
|
765
1356
|
: {}),
|
|
766
1357
|
...(isCustomLine ? { customLine: true } : {}),
|
|
767
1358
|
lineMode,
|
|
768
|
-
|
|
1359
|
+
...(resolvedQuantityUnit ? { quantityUnit: resolvedQuantityUnit } : {}),
|
|
1360
|
+
};
|
|
769
1361
|
|
|
770
1362
|
const payload: Record<string, unknown> = {
|
|
771
1363
|
[documentKey]: String(resolvedDocumentId),
|
|
772
1364
|
organizationId: String(resolvedOrg),
|
|
773
1365
|
tenantId: String(resolvedTenant),
|
|
774
|
-
productId: isCustomLine
|
|
775
|
-
|
|
1366
|
+
productId: isCustomLine
|
|
1367
|
+
? undefined
|
|
1368
|
+
: values.productId
|
|
1369
|
+
? String(values.productId)
|
|
1370
|
+
: undefined,
|
|
1371
|
+
productVariantId: isCustomLine
|
|
1372
|
+
? undefined
|
|
1373
|
+
: values.variantId
|
|
1374
|
+
? String(values.variantId)
|
|
1375
|
+
: undefined,
|
|
776
1376
|
quantity: qtyNumber,
|
|
1377
|
+
quantityUnit: resolvedQuantityUnit ?? undefined,
|
|
777
1378
|
currencyCode: String(resolvedCurrency),
|
|
778
|
-
priceId:
|
|
1379
|
+
priceId:
|
|
1380
|
+
!isCustomLine && values.priceId ? String(values.priceId) : undefined,
|
|
779
1381
|
priceMode: resolvedPriceMode,
|
|
780
1382
|
taxRate: Number.isFinite(resolvedTaxRate) ? resolvedTaxRate : undefined,
|
|
781
1383
|
unitPriceNet: safeUnitPriceNet,
|
|
@@ -785,41 +1387,38 @@ export function LineItemDialog({
|
|
|
785
1387
|
...(catalogSnapshot ? { catalogSnapshot } : {}),
|
|
786
1388
|
metadata,
|
|
787
1389
|
customFieldSetId: values.customFieldSetId ?? undefined,
|
|
788
|
-
...(typeof values.statusEntryId ===
|
|
1390
|
+
...(typeof values.statusEntryId === "string" &&
|
|
1391
|
+
values.statusEntryId.trim().length
|
|
789
1392
|
? { statusEntryId: values.statusEntryId.trim() }
|
|
790
1393
|
: {}),
|
|
791
|
-
}
|
|
1394
|
+
};
|
|
792
1395
|
|
|
793
1396
|
const customFields = collectCustomFieldValues(values, {
|
|
794
1397
|
transform: (value) => normalizeCustomFieldSubmitValue(value),
|
|
795
|
-
})
|
|
1398
|
+
});
|
|
796
1399
|
if (Object.keys(customFields).length) {
|
|
797
|
-
payload.customFields = normalizeCustomFieldValues(customFields)
|
|
1400
|
+
payload.customFields = normalizeCustomFieldValues(customFields);
|
|
798
1401
|
}
|
|
799
|
-
if (resolvedName) payload.name = resolvedName
|
|
800
|
-
|
|
801
|
-
console.debug('resolved scope', { resolvedDocumentId, resolvedOrg, resolvedTenant, resolvedCurrency })
|
|
802
|
-
console.debug('parsed numbers', { qtyNumber, unitPriceNumber })
|
|
803
|
-
console.log('sales.line.submit.payload', payload)
|
|
804
|
-
console.log('sales.line.submit.payload.json', JSON.stringify(payload))
|
|
805
|
-
console.groupEnd()
|
|
1402
|
+
if (resolvedName) payload.name = resolvedName;
|
|
806
1403
|
|
|
807
1404
|
try {
|
|
808
|
-
const action = editingId ? updateCrud : createCrud
|
|
1405
|
+
const action = editingId ? updateCrud : createCrud;
|
|
809
1406
|
const result = await action(
|
|
810
1407
|
resourcePath,
|
|
811
1408
|
editingId ? { id: editingId, ...payload } : payload,
|
|
812
1409
|
{
|
|
813
|
-
errorMessage: t(
|
|
1410
|
+
errorMessage: t(
|
|
1411
|
+
"sales.documents.items.errorSave",
|
|
1412
|
+
"Failed to save line.",
|
|
1413
|
+
),
|
|
814
1414
|
},
|
|
815
|
-
)
|
|
1415
|
+
);
|
|
816
1416
|
if (result.ok) {
|
|
817
|
-
if (onSaved) await onSaved()
|
|
818
|
-
closeDialog()
|
|
1417
|
+
if (onSaved) await onSaved();
|
|
1418
|
+
closeDialog();
|
|
819
1419
|
}
|
|
820
1420
|
} catch (err) {
|
|
821
|
-
|
|
822
|
-
throw err
|
|
1421
|
+
throw err;
|
|
823
1422
|
}
|
|
824
1423
|
},
|
|
825
1424
|
[
|
|
@@ -837,110 +1436,151 @@ export function LineItemDialog({
|
|
|
837
1436
|
resolvedOrganizationId,
|
|
838
1437
|
resolvedTenantId,
|
|
839
1438
|
],
|
|
840
|
-
)
|
|
1439
|
+
);
|
|
841
1440
|
|
|
842
1441
|
const fields = React.useMemo<CrudField[]>(() => {
|
|
843
|
-
const isCustomLine = lineMode ===
|
|
1442
|
+
const isCustomLine = lineMode === "custom";
|
|
844
1443
|
return [
|
|
845
1444
|
{
|
|
846
|
-
id:
|
|
847
|
-
label: t(
|
|
848
|
-
type:
|
|
849
|
-
layout:
|
|
1445
|
+
id: "lineMode",
|
|
1446
|
+
label: t("sales.documents.items.lineMode.label", "Line type"),
|
|
1447
|
+
type: "custom",
|
|
1448
|
+
layout: "full",
|
|
850
1449
|
component: ({ value, setValue, setFormValue }: FieldRenderProps) => {
|
|
851
|
-
const mode = value ===
|
|
852
|
-
const switchMode = (next:
|
|
853
|
-
if (next === mode) return
|
|
854
|
-
setValue(next)
|
|
855
|
-
setLineMode(next)
|
|
856
|
-
if (next ===
|
|
857
|
-
setProductOption(null)
|
|
858
|
-
setVariantOption(null)
|
|
859
|
-
setPriceOptions([])
|
|
860
|
-
|
|
861
|
-
setFormValue?.(
|
|
862
|
-
setFormValue?.(
|
|
863
|
-
setFormValue?.(
|
|
1450
|
+
const mode = value === "custom" ? "custom" : "catalog";
|
|
1451
|
+
const switchMode = (next: "catalog" | "custom") => {
|
|
1452
|
+
if (next === mode) return;
|
|
1453
|
+
setValue(next);
|
|
1454
|
+
setLineMode(next);
|
|
1455
|
+
if (next === "custom") {
|
|
1456
|
+
setProductOption(null);
|
|
1457
|
+
setVariantOption(null);
|
|
1458
|
+
setPriceOptions([]);
|
|
1459
|
+
setUnitOptions([]);
|
|
1460
|
+
setFormValue?.("productId", null);
|
|
1461
|
+
setFormValue?.("variantId", null);
|
|
1462
|
+
setFormValue?.("priceId", null);
|
|
1463
|
+
setFormValue?.("catalogSnapshot", null);
|
|
1464
|
+
setFormValue?.("quantityUnit", null);
|
|
864
1465
|
} else {
|
|
865
|
-
setFormValue?.(
|
|
866
|
-
setFormValue?.(
|
|
1466
|
+
setFormValue?.("unitPrice", "");
|
|
1467
|
+
setFormValue?.("priceMode", "gross");
|
|
867
1468
|
}
|
|
868
|
-
}
|
|
1469
|
+
};
|
|
869
1470
|
return (
|
|
870
1471
|
<div className="flex flex-col gap-2">
|
|
871
1472
|
<div className="inline-flex w-fit gap-1 rounded-md border bg-muted/50 p-1">
|
|
872
1473
|
<Button
|
|
873
1474
|
type="button"
|
|
874
1475
|
size="sm"
|
|
875
|
-
variant={mode ===
|
|
876
|
-
onClick={() => switchMode(
|
|
1476
|
+
variant={mode === "catalog" ? "default" : "ghost"}
|
|
1477
|
+
onClick={() => switchMode("catalog")}
|
|
877
1478
|
>
|
|
878
|
-
{t(
|
|
1479
|
+
{t("sales.documents.items.lineMode.catalog", "Catalog item")}
|
|
879
1480
|
</Button>
|
|
880
1481
|
<Button
|
|
881
1482
|
type="button"
|
|
882
1483
|
size="sm"
|
|
883
|
-
variant={mode ===
|
|
884
|
-
onClick={() => switchMode(
|
|
1484
|
+
variant={mode === "custom" ? "default" : "ghost"}
|
|
1485
|
+
onClick={() => switchMode("custom")}
|
|
885
1486
|
>
|
|
886
|
-
{t(
|
|
1487
|
+
{t("sales.documents.items.lineMode.custom", "Custom line")}
|
|
887
1488
|
</Button>
|
|
888
1489
|
</div>
|
|
889
1490
|
<p className="text-xs text-muted-foreground">
|
|
890
1491
|
{t(
|
|
891
|
-
|
|
892
|
-
|
|
1492
|
+
"sales.documents.items.lineMode.helper",
|
|
1493
|
+
"Use catalog products or create a freeform line with your own price.",
|
|
893
1494
|
)}
|
|
894
1495
|
</p>
|
|
895
1496
|
</div>
|
|
896
|
-
)
|
|
1497
|
+
);
|
|
897
1498
|
},
|
|
898
1499
|
} satisfies CrudField,
|
|
899
1500
|
...(!isCustomLine
|
|
900
1501
|
? [
|
|
901
1502
|
{
|
|
902
|
-
id:
|
|
903
|
-
label: t(
|
|
904
|
-
type:
|
|
1503
|
+
id: "productId",
|
|
1504
|
+
label: t("sales.documents.items.product", "Product"),
|
|
1505
|
+
type: "custom",
|
|
905
1506
|
required: true,
|
|
906
|
-
layout:
|
|
907
|
-
component: ({
|
|
1507
|
+
layout: "half",
|
|
1508
|
+
component: ({
|
|
1509
|
+
value,
|
|
1510
|
+
setValue,
|
|
1511
|
+
setFormValue,
|
|
1512
|
+
values,
|
|
1513
|
+
}: FieldRenderProps) => (
|
|
908
1514
|
<LookupSelect
|
|
909
|
-
value={typeof value ===
|
|
1515
|
+
value={typeof value === "string" ? value : null}
|
|
910
1516
|
onChange={(next) => {
|
|
911
|
-
const selectedOption = next
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
setFormValue?.(
|
|
1517
|
+
const selectedOption = next
|
|
1518
|
+
? (productOptionsRef.current.get(next) ?? null)
|
|
1519
|
+
: null;
|
|
1520
|
+
setProductOption(selectedOption);
|
|
1521
|
+
setVariantOption(null);
|
|
1522
|
+
setPriceOptions([]);
|
|
1523
|
+
setUnitOptions([]);
|
|
1524
|
+
setValue(next ?? null);
|
|
1525
|
+
setFormValue?.("variantId", null);
|
|
1526
|
+
setFormValue?.("priceId", null);
|
|
1527
|
+
setFormValue?.("unitPrice", "");
|
|
1528
|
+
setFormValue?.("priceMode", "gross");
|
|
1529
|
+
const defaultQuantityUnit =
|
|
1530
|
+
selectedOption?.defaultSalesUnit ??
|
|
1531
|
+
selectedOption?.defaultUnit ??
|
|
1532
|
+
null;
|
|
1533
|
+
setFormValue?.("quantityUnit", defaultQuantityUnit);
|
|
1534
|
+
if (
|
|
1535
|
+
typeof selectedOption?.defaultSalesUnitQuantity ===
|
|
1536
|
+
"number" &&
|
|
1537
|
+
Number.isFinite(
|
|
1538
|
+
selectedOption.defaultSalesUnitQuantity,
|
|
1539
|
+
) &&
|
|
1540
|
+
selectedOption.defaultSalesUnitQuantity > 0
|
|
1541
|
+
) {
|
|
1542
|
+
setFormValue?.(
|
|
1543
|
+
"quantity",
|
|
1544
|
+
String(selectedOption.defaultSalesUnitQuantity),
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
920
1547
|
const taxSelection = selectedOption
|
|
921
1548
|
? resolveTaxSelection(selectedOption)
|
|
922
|
-
: { taxRate: null, taxRateId: null }
|
|
923
|
-
setFormValue?.(
|
|
924
|
-
setFormValue?.(
|
|
925
|
-
const existingName =
|
|
1549
|
+
: { taxRate: null, taxRateId: null };
|
|
1550
|
+
setFormValue?.("taxRate", taxSelection.taxRate ?? null);
|
|
1551
|
+
setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
|
|
1552
|
+
const existingName =
|
|
1553
|
+
typeof values?.name === "string" ? values.name : "";
|
|
926
1554
|
if (!existingName.trim() && selectedOption?.title) {
|
|
927
|
-
setFormValue?.(
|
|
1555
|
+
setFormValue?.("name", selectedOption.title);
|
|
928
1556
|
}
|
|
929
1557
|
setFormValue?.(
|
|
930
|
-
|
|
1558
|
+
"catalogSnapshot",
|
|
931
1559
|
next
|
|
932
1560
|
? {
|
|
933
1561
|
product: {
|
|
934
1562
|
id: next,
|
|
935
1563
|
title: selectedOption?.title ?? null,
|
|
936
1564
|
sku: selectedOption?.sku ?? null,
|
|
937
|
-
thumbnailUrl:
|
|
1565
|
+
thumbnailUrl:
|
|
1566
|
+
selectedOption?.thumbnailUrl ?? null,
|
|
1567
|
+
defaultUnit: selectedOption?.defaultUnit ?? null,
|
|
1568
|
+
defaultSalesUnit:
|
|
1569
|
+
selectedOption?.defaultSalesUnit ?? null,
|
|
938
1570
|
},
|
|
939
1571
|
}
|
|
940
1572
|
: null,
|
|
941
|
-
)
|
|
1573
|
+
);
|
|
942
1574
|
if (next) {
|
|
943
|
-
void
|
|
1575
|
+
void loadProductUnits(next, selectedOption);
|
|
1576
|
+
void loadPrices(
|
|
1577
|
+
next,
|
|
1578
|
+
null,
|
|
1579
|
+
typeof values?.quantity === "string"
|
|
1580
|
+
? values.quantity
|
|
1581
|
+
: 1,
|
|
1582
|
+
defaultQuantityUnit,
|
|
1583
|
+
);
|
|
944
1584
|
}
|
|
945
1585
|
}}
|
|
946
1586
|
fetchItems={loadProductOptions}
|
|
@@ -951,65 +1591,106 @@ export function LineItemDialog({
|
|
|
951
1591
|
id: productOption.id,
|
|
952
1592
|
title: productOption.title || productOption.id,
|
|
953
1593
|
subtitle: productOption.sku ?? undefined,
|
|
954
|
-
icon: productOption.thumbnailUrl
|
|
955
|
-
|
|
956
|
-
|
|
1594
|
+
icon: productOption.thumbnailUrl ? (
|
|
1595
|
+
<img
|
|
1596
|
+
src={productOption.thumbnailUrl}
|
|
1597
|
+
alt={productOption.title ?? productOption.id}
|
|
1598
|
+
className="h-8 w-8 rounded object-cover"
|
|
1599
|
+
/>
|
|
1600
|
+
) : (
|
|
1601
|
+
buildPlaceholder(
|
|
1602
|
+
productOption.title || productOption.id,
|
|
1603
|
+
)
|
|
1604
|
+
),
|
|
957
1605
|
},
|
|
958
1606
|
]
|
|
959
1607
|
: undefined
|
|
960
1608
|
}
|
|
961
1609
|
minQuery={1}
|
|
962
|
-
searchPlaceholder={t(
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1610
|
+
searchPlaceholder={t(
|
|
1611
|
+
"sales.documents.items.productSearch",
|
|
1612
|
+
"Search product",
|
|
1613
|
+
)}
|
|
1614
|
+
selectLabel={t("ui.lookupSelect.select", "Select")}
|
|
1615
|
+
selectedLabel={t("ui.lookupSelect.selected", "Selected")}
|
|
1616
|
+
clearLabel={t(
|
|
1617
|
+
"ui.lookupSelect.clearSelection",
|
|
1618
|
+
"Clear selection",
|
|
1619
|
+
)}
|
|
1620
|
+
emptyLabel={t("ui.lookupSelect.noResults", "No results")}
|
|
1621
|
+
loadingLabel={t("ui.lookupSelect.searching", "Searching…")}
|
|
1622
|
+
startTypingLabel={t(
|
|
1623
|
+
"ui.lookupSelect.startTyping",
|
|
1624
|
+
"Start typing to search.",
|
|
1625
|
+
)}
|
|
969
1626
|
selectedHintLabel={(id) =>
|
|
970
|
-
t(
|
|
971
|
-
|
|
972
|
-
|
|
1627
|
+
t(
|
|
1628
|
+
"sales.documents.items.selectedProduct",
|
|
1629
|
+
"Selected {{id}}",
|
|
1630
|
+
{
|
|
1631
|
+
id: productOption?.title ?? id,
|
|
1632
|
+
},
|
|
1633
|
+
)
|
|
973
1634
|
}
|
|
974
1635
|
/>
|
|
975
1636
|
),
|
|
976
1637
|
} satisfies CrudField,
|
|
977
1638
|
{
|
|
978
|
-
id:
|
|
979
|
-
label: t(
|
|
980
|
-
type:
|
|
1639
|
+
id: "variantId",
|
|
1640
|
+
label: t("sales.documents.items.variant", "Variant"),
|
|
1641
|
+
type: "custom",
|
|
981
1642
|
required: true,
|
|
982
|
-
layout:
|
|
983
|
-
component: ({
|
|
984
|
-
|
|
1643
|
+
layout: "half",
|
|
1644
|
+
component: ({
|
|
1645
|
+
value,
|
|
1646
|
+
setValue,
|
|
1647
|
+
setFormValue,
|
|
1648
|
+
values,
|
|
1649
|
+
}: FieldRenderProps) => {
|
|
1650
|
+
const productId =
|
|
1651
|
+
typeof values?.productId === "string"
|
|
1652
|
+
? values.productId
|
|
1653
|
+
: null;
|
|
985
1654
|
return (
|
|
986
1655
|
<LookupSelect
|
|
987
|
-
key={productId ??
|
|
988
|
-
value={typeof value ===
|
|
1656
|
+
key={productId ?? "no-product"}
|
|
1657
|
+
value={typeof value === "string" ? value : null}
|
|
989
1658
|
onChange={(next) => {
|
|
990
|
-
const selectedOption = next
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1659
|
+
const selectedOption = next
|
|
1660
|
+
? (variantOptionsRef.current.get(next) ?? null)
|
|
1661
|
+
: null;
|
|
1662
|
+
setVariantOption(selectedOption);
|
|
1663
|
+
setValue(next ?? null);
|
|
1664
|
+
const existingName =
|
|
1665
|
+
typeof values?.name === "string" ? values.name : "";
|
|
994
1666
|
if (!existingName.trim()) {
|
|
995
|
-
setFormValue?.(
|
|
1667
|
+
setFormValue?.(
|
|
1668
|
+
"name",
|
|
1669
|
+
selectedOption?.title ??
|
|
1670
|
+
productOption?.title ??
|
|
1671
|
+
existingName,
|
|
1672
|
+
);
|
|
996
1673
|
}
|
|
997
1674
|
const taxSource = hasTaxMetadata(selectedOption)
|
|
998
1675
|
? selectedOption
|
|
999
1676
|
: hasTaxMetadata(productOption)
|
|
1000
1677
|
? productOption
|
|
1001
|
-
: null
|
|
1678
|
+
: null;
|
|
1002
1679
|
if (taxSource) {
|
|
1003
|
-
const taxSelection = resolveTaxSelection(taxSource)
|
|
1004
|
-
setFormValue?.(
|
|
1005
|
-
setFormValue?.(
|
|
1680
|
+
const taxSelection = resolveTaxSelection(taxSource);
|
|
1681
|
+
setFormValue?.("taxRate", taxSelection.taxRate ?? null);
|
|
1682
|
+
setFormValue?.(
|
|
1683
|
+
"taxRateId",
|
|
1684
|
+
taxSelection.taxRateId ?? null,
|
|
1685
|
+
);
|
|
1006
1686
|
}
|
|
1007
1687
|
const prevSnapshot =
|
|
1008
|
-
typeof values?.catalogSnapshot ===
|
|
1688
|
+
typeof values?.catalogSnapshot === "object" &&
|
|
1689
|
+
values.catalogSnapshot
|
|
1009
1690
|
? (values.catalogSnapshot as Record<string, unknown>)
|
|
1010
|
-
: null
|
|
1691
|
+
: null;
|
|
1011
1692
|
if (next) {
|
|
1012
|
-
setFormValue?.(
|
|
1693
|
+
setFormValue?.("catalogSnapshot", {
|
|
1013
1694
|
...(prevSnapshot ?? {}),
|
|
1014
1695
|
variant: {
|
|
1015
1696
|
id: next,
|
|
@@ -1017,34 +1698,67 @@ export function LineItemDialog({
|
|
|
1017
1698
|
sku: selectedOption?.sku ?? null,
|
|
1018
1699
|
thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
|
|
1019
1700
|
},
|
|
1020
|
-
})
|
|
1701
|
+
});
|
|
1021
1702
|
} else if (prevSnapshot) {
|
|
1022
|
-
const snapshot = { ...prevSnapshot }
|
|
1023
|
-
if (
|
|
1024
|
-
|
|
1703
|
+
const snapshot = { ...prevSnapshot };
|
|
1704
|
+
if ("variant" in snapshot)
|
|
1705
|
+
delete (snapshot as CatalogSnapshotRecord).variant;
|
|
1706
|
+
setFormValue?.(
|
|
1707
|
+
"catalogSnapshot",
|
|
1708
|
+
Object.keys(snapshot).length ? snapshot : null,
|
|
1709
|
+
);
|
|
1025
1710
|
} else {
|
|
1026
|
-
setFormValue?.(
|
|
1711
|
+
setFormValue?.("catalogSnapshot", null);
|
|
1027
1712
|
}
|
|
1028
1713
|
if (productId) {
|
|
1029
|
-
|
|
1714
|
+
const currentQuantity =
|
|
1715
|
+
typeof values?.quantity === "string"
|
|
1716
|
+
? values.quantity
|
|
1717
|
+
: 1;
|
|
1718
|
+
const currentQuantityUnit =
|
|
1719
|
+
typeof values?.quantityUnit === "string"
|
|
1720
|
+
? values.quantityUnit
|
|
1721
|
+
: null;
|
|
1722
|
+
void loadPrices(
|
|
1723
|
+
productId,
|
|
1724
|
+
next,
|
|
1725
|
+
currentQuantity,
|
|
1726
|
+
currentQuantityUnit,
|
|
1727
|
+
);
|
|
1030
1728
|
}
|
|
1031
1729
|
}}
|
|
1032
1730
|
fetchItems={async (query) => {
|
|
1033
|
-
if (!productId) return []
|
|
1034
|
-
const productThumb = productId
|
|
1035
|
-
|
|
1036
|
-
|
|
1731
|
+
if (!productId) return [];
|
|
1732
|
+
const productThumb = productId
|
|
1733
|
+
? productOptionsRef.current.get(productId)?.thumbnailUrl
|
|
1734
|
+
: null;
|
|
1735
|
+
const options = await loadVariantOptions(
|
|
1736
|
+
productId,
|
|
1737
|
+
productThumb,
|
|
1738
|
+
);
|
|
1739
|
+
const needle = query?.trim().toLowerCase() ?? "";
|
|
1037
1740
|
return needle.length
|
|
1038
|
-
? options.filter((option) =>
|
|
1039
|
-
|
|
1741
|
+
? options.filter((option) =>
|
|
1742
|
+
option.title.toLowerCase().includes(needle),
|
|
1743
|
+
)
|
|
1744
|
+
: options;
|
|
1040
1745
|
}}
|
|
1041
|
-
searchPlaceholder={t(
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1746
|
+
searchPlaceholder={t(
|
|
1747
|
+
"sales.documents.items.variantSearch",
|
|
1748
|
+
"Search variant",
|
|
1749
|
+
)}
|
|
1750
|
+
selectLabel={t("ui.lookupSelect.select", "Select")}
|
|
1751
|
+
selectedLabel={t("ui.lookupSelect.selected", "Selected")}
|
|
1752
|
+
clearLabel={t(
|
|
1753
|
+
"ui.lookupSelect.clearSelection",
|
|
1754
|
+
"Clear selection",
|
|
1755
|
+
)}
|
|
1756
|
+
emptyLabel={t("ui.lookupSelect.noResults", "No results")}
|
|
1757
|
+
loadingLabel={t("ui.lookupSelect.searching", "Searching…")}
|
|
1758
|
+
startTypingLabel={t(
|
|
1759
|
+
"ui.lookupSelect.startTyping",
|
|
1760
|
+
"Start typing to search.",
|
|
1761
|
+
)}
|
|
1048
1762
|
minQuery={0}
|
|
1049
1763
|
options={
|
|
1050
1764
|
variantOption
|
|
@@ -1060,64 +1774,88 @@ export function LineItemDialog({
|
|
|
1060
1774
|
className="h-8 w-8 rounded object-cover"
|
|
1061
1775
|
/>
|
|
1062
1776
|
) : (
|
|
1063
|
-
buildPlaceholder(
|
|
1777
|
+
buildPlaceholder(
|
|
1778
|
+
variantOption.title || variantOption.id,
|
|
1779
|
+
)
|
|
1064
1780
|
),
|
|
1065
1781
|
},
|
|
1066
1782
|
]
|
|
1067
1783
|
: undefined
|
|
1068
1784
|
}
|
|
1069
1785
|
selectedHintLabel={(id) =>
|
|
1070
|
-
t(
|
|
1071
|
-
|
|
1072
|
-
|
|
1786
|
+
t(
|
|
1787
|
+
"sales.documents.items.selectedVariant",
|
|
1788
|
+
"Selected {{id}}",
|
|
1789
|
+
{
|
|
1790
|
+
id: variantOption?.title ?? id,
|
|
1791
|
+
},
|
|
1792
|
+
)
|
|
1073
1793
|
}
|
|
1074
1794
|
disabled={!productId}
|
|
1075
1795
|
/>
|
|
1076
|
-
)
|
|
1796
|
+
);
|
|
1077
1797
|
},
|
|
1078
1798
|
} satisfies CrudField,
|
|
1079
1799
|
{
|
|
1080
|
-
id:
|
|
1081
|
-
label: t(
|
|
1082
|
-
type:
|
|
1083
|
-
layout:
|
|
1084
|
-
component: ({
|
|
1085
|
-
|
|
1086
|
-
|
|
1800
|
+
id: "priceId",
|
|
1801
|
+
label: t("sales.documents.items.price", "Price"),
|
|
1802
|
+
type: "custom",
|
|
1803
|
+
layout: "half",
|
|
1804
|
+
component: ({
|
|
1805
|
+
value,
|
|
1806
|
+
setValue,
|
|
1807
|
+
setFormValue,
|
|
1808
|
+
values,
|
|
1809
|
+
}: FieldRenderProps) => {
|
|
1810
|
+
const productId =
|
|
1811
|
+
typeof values?.productId === "string"
|
|
1812
|
+
? values.productId
|
|
1813
|
+
: null;
|
|
1814
|
+
const variantId =
|
|
1815
|
+
typeof values?.variantId === "string"
|
|
1816
|
+
? values.variantId
|
|
1817
|
+
: null;
|
|
1087
1818
|
return (
|
|
1088
1819
|
<LookupSelect
|
|
1089
|
-
key={
|
|
1090
|
-
|
|
1820
|
+
key={
|
|
1821
|
+
productId
|
|
1822
|
+
? `${productId}-${variantId ?? "no-variant"}`
|
|
1823
|
+
: "price"
|
|
1824
|
+
}
|
|
1825
|
+
value={typeof value === "string" ? value : null}
|
|
1091
1826
|
onChange={(next) => {
|
|
1092
|
-
setValue(next ?? null)
|
|
1093
|
-
const selected = next
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
)
|
|
1109
|
-
} else {
|
|
1110
|
-
const fallbackTax = resolveTaxSelection(variantOption ?? productOption ?? null)
|
|
1111
|
-
setFormValue?.('taxRate', fallbackTax.taxRate ?? null)
|
|
1112
|
-
setFormValue?.('taxRateId', fallbackTax.taxRateId ?? null)
|
|
1113
|
-
}
|
|
1827
|
+
setValue(next ?? null);
|
|
1828
|
+
const selected = next
|
|
1829
|
+
? (priceOptions.find((entry) => entry.id === next) ??
|
|
1830
|
+
null)
|
|
1831
|
+
: null;
|
|
1832
|
+
applyPriceSelection(
|
|
1833
|
+
selected,
|
|
1834
|
+
setFormValue,
|
|
1835
|
+
{
|
|
1836
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
1837
|
+
quantityUnit:
|
|
1838
|
+
typeof values?.quantityUnit === "string"
|
|
1839
|
+
? values.quantityUnit
|
|
1840
|
+
: null,
|
|
1841
|
+
},
|
|
1842
|
+
);
|
|
1114
1843
|
}}
|
|
1115
1844
|
fetchItems={async (query) => {
|
|
1116
|
-
const prices = await loadPrices(
|
|
1117
|
-
|
|
1845
|
+
const prices = await loadPrices(
|
|
1846
|
+
productId,
|
|
1847
|
+
variantId,
|
|
1848
|
+
typeof values?.quantity === "string"
|
|
1849
|
+
? values.quantity
|
|
1850
|
+
: 1,
|
|
1851
|
+
typeof values?.quantityUnit === "string"
|
|
1852
|
+
? values.quantityUnit
|
|
1853
|
+
: null,
|
|
1854
|
+
);
|
|
1855
|
+
const needle = query?.trim().toLowerCase() ?? "";
|
|
1118
1856
|
return prices
|
|
1119
1857
|
.filter((price) => {
|
|
1120
|
-
if (!needle.length) return true
|
|
1858
|
+
if (!needle.length) return true;
|
|
1121
1859
|
const haystack = [
|
|
1122
1860
|
price.label,
|
|
1123
1861
|
price.priceKindTitle,
|
|
@@ -1126,99 +1864,208 @@ export function LineItemDialog({
|
|
|
1126
1864
|
...(price.scopeTags ?? []),
|
|
1127
1865
|
]
|
|
1128
1866
|
.filter(Boolean)
|
|
1129
|
-
.join(
|
|
1130
|
-
.toLowerCase()
|
|
1131
|
-
return haystack.includes(needle)
|
|
1867
|
+
.join(" ")
|
|
1868
|
+
.toLowerCase();
|
|
1869
|
+
return haystack.includes(needle);
|
|
1132
1870
|
})
|
|
1133
1871
|
.map<LookupSelectItem>((price) => ({
|
|
1134
1872
|
id: price.id,
|
|
1135
1873
|
title: price.label,
|
|
1136
|
-
subtitle:
|
|
1874
|
+
subtitle:
|
|
1875
|
+
price.priceKindTitle ??
|
|
1876
|
+
price.priceKindCode ??
|
|
1877
|
+
undefined,
|
|
1137
1878
|
description: price.scopeReason ?? undefined,
|
|
1138
1879
|
rightLabel: price.currencyCode ?? undefined,
|
|
1139
|
-
icon:
|
|
1140
|
-
|
|
1880
|
+
icon: (
|
|
1881
|
+
<DollarSign className="h-5 w-5 text-muted-foreground" />
|
|
1882
|
+
),
|
|
1883
|
+
}));
|
|
1141
1884
|
}}
|
|
1142
1885
|
minQuery={0}
|
|
1143
1886
|
loading={priceLoading}
|
|
1144
|
-
searchPlaceholder={t(
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1887
|
+
searchPlaceholder={t(
|
|
1888
|
+
"sales.documents.items.priceSearch",
|
|
1889
|
+
"Select price",
|
|
1890
|
+
)}
|
|
1891
|
+
selectLabel={t("ui.lookupSelect.select", "Select")}
|
|
1892
|
+
selectedLabel={t("ui.lookupSelect.selected", "Selected")}
|
|
1893
|
+
clearLabel={t(
|
|
1894
|
+
"ui.lookupSelect.clearSelection",
|
|
1895
|
+
"Clear selection",
|
|
1896
|
+
)}
|
|
1897
|
+
emptyLabel={t("ui.lookupSelect.noResults", "No results")}
|
|
1898
|
+
loadingLabel={t("ui.lookupSelect.searching", "Searching…")}
|
|
1899
|
+
startTypingLabel={t(
|
|
1900
|
+
"ui.lookupSelect.startTyping",
|
|
1901
|
+
"Start typing to search.",
|
|
1902
|
+
)}
|
|
1151
1903
|
disabled={!productId}
|
|
1152
1904
|
/>
|
|
1153
|
-
)
|
|
1905
|
+
);
|
|
1154
1906
|
},
|
|
1155
1907
|
} satisfies CrudField,
|
|
1156
1908
|
]
|
|
1157
1909
|
: []),
|
|
1158
1910
|
{
|
|
1159
|
-
id:
|
|
1160
|
-
label: t(
|
|
1161
|
-
type:
|
|
1162
|
-
layout:
|
|
1163
|
-
component: ({
|
|
1164
|
-
|
|
1911
|
+
id: "unitPrice",
|
|
1912
|
+
label: t("sales.documents.items.unitPrice", "Unit price"),
|
|
1913
|
+
type: "custom",
|
|
1914
|
+
layout: "half",
|
|
1915
|
+
component: ({
|
|
1916
|
+
value,
|
|
1917
|
+
setValue,
|
|
1918
|
+
setFormValue,
|
|
1919
|
+
values,
|
|
1920
|
+
}: FieldRenderProps) => {
|
|
1921
|
+
const mode = values?.priceMode === "net" ? "net" : "gross";
|
|
1922
|
+
const selectedPriceId =
|
|
1923
|
+
typeof values?.priceId === "string" ? values.priceId : null;
|
|
1924
|
+
const selectedPrice = selectedPriceId
|
|
1925
|
+
? (priceOptions.find((entry) => entry.id === selectedPriceId) ?? null)
|
|
1926
|
+
: null;
|
|
1927
|
+
const selectedCurrency =
|
|
1928
|
+
selectedPrice?.currencyCode ??
|
|
1929
|
+
(typeof values?.currencyCode === "string" ? values.currencyCode : null);
|
|
1930
|
+
const quantityUnitCode = normalizeUnitCode(values?.quantityUnit);
|
|
1931
|
+
const baseUnitCode =
|
|
1932
|
+
unitOptions.find((option) => option.isBase)?.code ?? null;
|
|
1933
|
+
const selectedUnitOption = quantityUnitCode
|
|
1934
|
+
? (unitOptions.find((option) => option.code === quantityUnitCode) ?? null)
|
|
1935
|
+
: null;
|
|
1936
|
+
const unitFactor = (() => {
|
|
1937
|
+
if (!quantityUnitCode) return null;
|
|
1938
|
+
if (baseUnitCode && quantityUnitCode === baseUnitCode) return 1;
|
|
1939
|
+
const value = normalizeNumber(selectedUnitOption?.toBaseFactor, Number.NaN);
|
|
1940
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
1941
|
+
})();
|
|
1942
|
+
const selectedBaseAmount = selectedPrice
|
|
1943
|
+
? mode === "net"
|
|
1944
|
+
? (selectedPrice.amountNet ?? selectedPrice.amountGross ?? null)
|
|
1945
|
+
: (selectedPrice.amountGross ?? selectedPrice.amountNet ?? null)
|
|
1946
|
+
: null;
|
|
1947
|
+
const convertedAmount =
|
|
1948
|
+
selectedBaseAmount !== null &&
|
|
1949
|
+
unitFactor !== null &&
|
|
1950
|
+
Number.isFinite(selectedBaseAmount * unitFactor)
|
|
1951
|
+
? selectedBaseAmount * unitFactor
|
|
1952
|
+
: null;
|
|
1953
|
+
const isCatalogLine = lineMode !== "custom";
|
|
1165
1954
|
return (
|
|
1166
|
-
<div className="
|
|
1167
|
-
<
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1955
|
+
<div className="space-y-2">
|
|
1956
|
+
<div className="flex gap-2">
|
|
1957
|
+
<Input
|
|
1958
|
+
value={
|
|
1959
|
+
typeof value === "string"
|
|
1960
|
+
? value
|
|
1961
|
+
: value == null
|
|
1962
|
+
? ""
|
|
1963
|
+
: String(value)
|
|
1964
|
+
}
|
|
1965
|
+
onChange={(event) => setValue(event.target.value)}
|
|
1966
|
+
placeholder="0.00"
|
|
1967
|
+
/>
|
|
1968
|
+
<select
|
|
1969
|
+
className="w-32 rounded border px-2 text-sm"
|
|
1970
|
+
value={mode}
|
|
1971
|
+
onChange={(event) => {
|
|
1972
|
+
const nextMode =
|
|
1973
|
+
event.target.value === "net" ? "net" : "gross";
|
|
1974
|
+
setFormValue?.("priceMode", nextMode);
|
|
1975
|
+
}}
|
|
1976
|
+
>
|
|
1977
|
+
<option value="gross">
|
|
1978
|
+
{t("sales.documents.items.priceGross", "Gross")}
|
|
1979
|
+
</option>
|
|
1980
|
+
<option value="net">
|
|
1981
|
+
{t("sales.documents.items.priceNet", "Net")}
|
|
1982
|
+
</option>
|
|
1983
|
+
</select>
|
|
1984
|
+
</div>
|
|
1985
|
+
{isCatalogLine && selectedPrice && quantityUnitCode && baseUnitCode ? (
|
|
1986
|
+
unitFactor !== null && convertedAmount !== null ? (
|
|
1987
|
+
<p className="text-xs text-muted-foreground">
|
|
1988
|
+
{t(
|
|
1989
|
+
"sales.documents.items.priceBasisTemplate",
|
|
1990
|
+
"Catalog price basis: {{baseAmount}} / {{baseUnit}}. Converted for {{unit}}: {{baseAmount}} × {{factor}} = {{convertedAmount}}.",
|
|
1991
|
+
{
|
|
1992
|
+
baseAmount: formatMoney(selectedBaseAmount as number, selectedCurrency),
|
|
1993
|
+
baseUnit: baseUnitCode,
|
|
1994
|
+
unit: quantityUnitCode,
|
|
1995
|
+
factor: unitFactor,
|
|
1996
|
+
convertedAmount: formatMoney(convertedAmount, selectedCurrency),
|
|
1997
|
+
},
|
|
1998
|
+
)}
|
|
1999
|
+
</p>
|
|
2000
|
+
) : (
|
|
2001
|
+
<p className="text-xs text-muted-foreground">
|
|
2002
|
+
{t(
|
|
2003
|
+
"sales.documents.items.priceBasisMissingConversion",
|
|
2004
|
+
"Catalog price basis is base unit, but conversion for selected unit is missing.",
|
|
2005
|
+
)}
|
|
2006
|
+
</p>
|
|
2007
|
+
)
|
|
2008
|
+
) : null}
|
|
2009
|
+
{isCatalogLine && !selectedPrice ? (
|
|
2010
|
+
<p className="text-xs text-muted-foreground">
|
|
2011
|
+
{t(
|
|
2012
|
+
"sales.documents.items.priceBasisManual",
|
|
2013
|
+
"Manual unit price mode. Price will not auto-convert until you select a catalog price.",
|
|
2014
|
+
)}
|
|
2015
|
+
</p>
|
|
2016
|
+
) : null}
|
|
1183
2017
|
</div>
|
|
1184
|
-
)
|
|
2018
|
+
);
|
|
1185
2019
|
},
|
|
1186
2020
|
} satisfies CrudField,
|
|
1187
2021
|
{
|
|
1188
|
-
id:
|
|
1189
|
-
label: t(
|
|
1190
|
-
type:
|
|
1191
|
-
layout:
|
|
1192
|
-
component: ({
|
|
2022
|
+
id: "taxRateId",
|
|
2023
|
+
label: t("sales.documents.items.taxRate", "Tax class"),
|
|
2024
|
+
type: "custom",
|
|
2025
|
+
layout: "half",
|
|
2026
|
+
component: ({
|
|
2027
|
+
value,
|
|
2028
|
+
setValue,
|
|
2029
|
+
setFormValue,
|
|
2030
|
+
values,
|
|
2031
|
+
}: FieldRenderProps) => {
|
|
1193
2032
|
const resolvedValue =
|
|
1194
|
-
typeof value ===
|
|
2033
|
+
typeof value === "string" && value.trim().length
|
|
1195
2034
|
? value
|
|
1196
|
-
: findTaxRateIdByValue((values as
|
|
1197
|
-
const handleChange = (
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
2035
|
+
: findTaxRateIdByValue((values as Record<string, unknown>)?.taxRate as number | null | undefined);
|
|
2036
|
+
const handleChange = (
|
|
2037
|
+
event: React.ChangeEvent<HTMLSelectElement>,
|
|
2038
|
+
) => {
|
|
2039
|
+
const nextId = event.target.value || null;
|
|
2040
|
+
const option = nextId ? (taxRateMap.get(nextId) ?? null) : null;
|
|
2041
|
+
setValue(nextId);
|
|
2042
|
+
const rate = normalizeNumber(option?.rate);
|
|
2043
|
+
setFormValue?.("taxRate", Number.isFinite(rate) ? rate : null);
|
|
2044
|
+
};
|
|
1204
2045
|
return (
|
|
1205
2046
|
<div className="flex items-center gap-2">
|
|
1206
2047
|
<select
|
|
1207
2048
|
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"
|
|
1208
|
-
value={resolvedValue ??
|
|
2049
|
+
value={resolvedValue ?? ""}
|
|
1209
2050
|
onChange={handleChange}
|
|
1210
2051
|
disabled={!taxRates.length}
|
|
1211
2052
|
>
|
|
1212
2053
|
<option value="">
|
|
1213
2054
|
{taxRates.length
|
|
1214
|
-
? t(
|
|
1215
|
-
|
|
2055
|
+
? t(
|
|
2056
|
+
"sales.documents.items.taxRate.none",
|
|
2057
|
+
"No tax class selected",
|
|
2058
|
+
)
|
|
2059
|
+
: t(
|
|
2060
|
+
"sales.documents.items.taxRate.empty",
|
|
2061
|
+
"No tax classes available",
|
|
2062
|
+
)}
|
|
1216
2063
|
</option>
|
|
1217
2064
|
{taxRates.map((rate) => (
|
|
1218
2065
|
<option key={rate.id} value={rate.id}>
|
|
1219
2066
|
{rate.name}
|
|
1220
|
-
{rate.code ? ` • ${rate.code.toUpperCase()}` :
|
|
1221
|
-
{Number.isFinite(rate.rate) ? ` • ${rate.rate}%` :
|
|
2067
|
+
{rate.code ? ` • ${rate.code.toUpperCase()}` : ""}
|
|
2068
|
+
{Number.isFinite(rate.rate) ? ` • ${rate.rate}%` : ""}
|
|
1222
2069
|
</option>
|
|
1223
2070
|
))}
|
|
1224
2071
|
</select>
|
|
@@ -1227,271 +2074,579 @@ export function LineItemDialog({
|
|
|
1227
2074
|
variant="ghost"
|
|
1228
2075
|
size="icon"
|
|
1229
2076
|
onClick={() => {
|
|
1230
|
-
if (typeof window !==
|
|
1231
|
-
window.open(
|
|
2077
|
+
if (typeof window !== "undefined") {
|
|
2078
|
+
window.open(
|
|
2079
|
+
"/backend/config/sales?section=tax-rates",
|
|
2080
|
+
"_blank",
|
|
2081
|
+
"noopener,noreferrer",
|
|
2082
|
+
);
|
|
1232
2083
|
}
|
|
1233
2084
|
}}
|
|
1234
|
-
title={t(
|
|
2085
|
+
title={t(
|
|
2086
|
+
"catalog.products.create.taxRates.manage",
|
|
2087
|
+
"Manage tax classes",
|
|
2088
|
+
)}
|
|
1235
2089
|
>
|
|
1236
2090
|
<Settings className="h-4 w-4" />
|
|
1237
2091
|
</Button>
|
|
1238
2092
|
</div>
|
|
1239
|
-
)
|
|
2093
|
+
);
|
|
1240
2094
|
},
|
|
1241
2095
|
} satisfies CrudField,
|
|
1242
2096
|
{
|
|
1243
|
-
id:
|
|
1244
|
-
label: t(
|
|
1245
|
-
type:
|
|
1246
|
-
layout:
|
|
1247
|
-
component: ({ value, setValue }: FieldRenderProps) =>
|
|
2097
|
+
id: "quantityUnit",
|
|
2098
|
+
label: t("sales.documents.items.quantityUnit", "Unit"),
|
|
2099
|
+
type: "custom",
|
|
2100
|
+
layout: "half",
|
|
2101
|
+
component: ({ value, setValue, setFormValue, values }: FieldRenderProps) => {
|
|
2102
|
+
const productId =
|
|
2103
|
+
typeof values?.productId === "string" ? values.productId : null;
|
|
2104
|
+
const variantId =
|
|
2105
|
+
typeof values?.variantId === "string" ? values.variantId : null;
|
|
2106
|
+
if (isCustomLine) {
|
|
2107
|
+
return (
|
|
2108
|
+
<Input
|
|
2109
|
+
value={typeof value === "string" ? value : ""}
|
|
2110
|
+
onChange={(event) => setValue(event.target.value || null)}
|
|
2111
|
+
placeholder={t(
|
|
2112
|
+
"sales.documents.items.quantityUnitPlaceholder",
|
|
2113
|
+
"e.g. pc",
|
|
2114
|
+
)}
|
|
2115
|
+
/>
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
return (
|
|
2119
|
+
<select
|
|
2120
|
+
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"
|
|
2121
|
+
value={typeof value === "string" ? value : ""}
|
|
2122
|
+
onChange={(event) => {
|
|
2123
|
+
const nextValue = event.target.value || null;
|
|
2124
|
+
const nextUnitPrice = convertUnitPriceForUnitChange(
|
|
2125
|
+
values?.unitPrice,
|
|
2126
|
+
typeof value === "string" ? value : null,
|
|
2127
|
+
nextValue,
|
|
2128
|
+
);
|
|
2129
|
+
setValue(nextValue);
|
|
2130
|
+
if (nextUnitPrice) {
|
|
2131
|
+
setFormValue?.("unitPrice", nextUnitPrice);
|
|
2132
|
+
}
|
|
2133
|
+
if (productId) {
|
|
2134
|
+
const selectedPriceId =
|
|
2135
|
+
typeof values?.priceId === "string" ? values.priceId : null;
|
|
2136
|
+
const selectedPriceKindId =
|
|
2137
|
+
selectedPriceId
|
|
2138
|
+
? (priceOptions.find(
|
|
2139
|
+
(entry) => entry.id === selectedPriceId,
|
|
2140
|
+
)?.priceKindId ?? null)
|
|
2141
|
+
: null;
|
|
2142
|
+
void loadPrices(
|
|
2143
|
+
productId,
|
|
2144
|
+
variantId,
|
|
2145
|
+
typeof values?.quantity === "string" ? values.quantity : 1,
|
|
2146
|
+
nextValue,
|
|
2147
|
+
).then((prices) => {
|
|
2148
|
+
if (!selectedPriceId) return;
|
|
2149
|
+
const nextSelected = selectPriceAfterRefresh(
|
|
2150
|
+
prices,
|
|
2151
|
+
selectedPriceId,
|
|
2152
|
+
selectedPriceKindId,
|
|
2153
|
+
);
|
|
2154
|
+
if (!nextSelected) {
|
|
2155
|
+
setFormValue?.("priceId", null);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
applyPriceSelection(
|
|
2159
|
+
nextSelected,
|
|
2160
|
+
setFormValue,
|
|
2161
|
+
{
|
|
2162
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
2163
|
+
quantityUnit: nextValue,
|
|
2164
|
+
},
|
|
2165
|
+
);
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}}
|
|
2169
|
+
disabled={!productId}
|
|
2170
|
+
>
|
|
2171
|
+
<option value="">
|
|
2172
|
+
{t("sales.documents.items.quantityUnitSelect", "Select unit")}
|
|
2173
|
+
</option>
|
|
2174
|
+
{unitOptions.map((option) => (
|
|
2175
|
+
<option key={option.code} value={option.code}>
|
|
2176
|
+
{option.isBase
|
|
2177
|
+
? `${option.code} (${t("sales.documents.items.baseUnitTag", "base")})`
|
|
2178
|
+
: option.code}
|
|
2179
|
+
</option>
|
|
2180
|
+
))}
|
|
2181
|
+
</select>
|
|
2182
|
+
);
|
|
2183
|
+
},
|
|
2184
|
+
} satisfies CrudField,
|
|
2185
|
+
{
|
|
2186
|
+
id: "quantity",
|
|
2187
|
+
label: t("sales.documents.items.quantity", "Quantity"),
|
|
2188
|
+
type: "custom",
|
|
2189
|
+
layout: "half",
|
|
2190
|
+
component: ({ value, setValue, setFormValue, values }: FieldRenderProps) => (
|
|
1248
2191
|
<Input
|
|
1249
|
-
value={
|
|
1250
|
-
|
|
2192
|
+
value={
|
|
2193
|
+
typeof value === "string"
|
|
2194
|
+
? value
|
|
2195
|
+
: value == null
|
|
2196
|
+
? ""
|
|
2197
|
+
: String(value)
|
|
2198
|
+
}
|
|
2199
|
+
onChange={(event) => {
|
|
2200
|
+
const nextQuantity = event.target.value;
|
|
2201
|
+
setValue(nextQuantity);
|
|
2202
|
+
const productId =
|
|
2203
|
+
typeof values?.productId === "string" ? values.productId : null;
|
|
2204
|
+
const variantId =
|
|
2205
|
+
typeof values?.variantId === "string" ? values.variantId : null;
|
|
2206
|
+
const quantityUnit =
|
|
2207
|
+
typeof values?.quantityUnit === "string"
|
|
2208
|
+
? values.quantityUnit
|
|
2209
|
+
: null;
|
|
2210
|
+
if (productId) {
|
|
2211
|
+
const selectedPriceId =
|
|
2212
|
+
typeof values?.priceId === "string" ? values.priceId : null;
|
|
2213
|
+
const selectedPriceKindId =
|
|
2214
|
+
selectedPriceId
|
|
2215
|
+
? (priceOptions.find((entry) => entry.id === selectedPriceId)
|
|
2216
|
+
?.priceKindId ?? null)
|
|
2217
|
+
: null;
|
|
2218
|
+
void loadPrices(
|
|
2219
|
+
productId,
|
|
2220
|
+
variantId,
|
|
2221
|
+
nextQuantity,
|
|
2222
|
+
quantityUnit,
|
|
2223
|
+
).then((prices) => {
|
|
2224
|
+
if (!selectedPriceId) return;
|
|
2225
|
+
const nextSelected = selectPriceAfterRefresh(
|
|
2226
|
+
prices,
|
|
2227
|
+
selectedPriceId,
|
|
2228
|
+
selectedPriceKindId,
|
|
2229
|
+
);
|
|
2230
|
+
if (!nextSelected) {
|
|
2231
|
+
setFormValue?.("priceId", null);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
applyPriceSelection(
|
|
2235
|
+
nextSelected,
|
|
2236
|
+
setFormValue,
|
|
2237
|
+
{
|
|
2238
|
+
fallbackTaxSource: variantOption ?? productOption ?? null,
|
|
2239
|
+
quantityUnit,
|
|
2240
|
+
},
|
|
2241
|
+
);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
}}
|
|
1251
2245
|
placeholder="1"
|
|
1252
2246
|
/>
|
|
1253
2247
|
),
|
|
1254
2248
|
} satisfies CrudField,
|
|
1255
2249
|
{
|
|
1256
|
-
id:
|
|
1257
|
-
label: t(
|
|
1258
|
-
type:
|
|
1259
|
-
layout:
|
|
2250
|
+
id: "uomPreview",
|
|
2251
|
+
label: t("sales.documents.items.uomPreview", "Normalized quantity"),
|
|
2252
|
+
type: "custom",
|
|
2253
|
+
layout: "full",
|
|
2254
|
+
component: ({ values }: FieldRenderProps) => {
|
|
2255
|
+
if (isCustomLine) return null;
|
|
2256
|
+
const quantity = normalizeNumber(values?.quantity, Number.NaN);
|
|
2257
|
+
const enteredUnit = normalizeUnitCode(values?.quantityUnit);
|
|
2258
|
+
if (!Number.isFinite(quantity) || quantity <= 0 || !enteredUnit) {
|
|
2259
|
+
return (
|
|
2260
|
+
<p className="text-xs text-muted-foreground">
|
|
2261
|
+
{t(
|
|
2262
|
+
"sales.documents.items.uomPreviewEmpty",
|
|
2263
|
+
"Select unit and quantity to preview normalization.",
|
|
2264
|
+
)}
|
|
2265
|
+
</p>
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
const selectedOption =
|
|
2269
|
+
unitOptions.find((option) => option.code === enteredUnit) ?? null;
|
|
2270
|
+
const baseOption =
|
|
2271
|
+
unitOptions.find((option) => option.isBase) ?? null;
|
|
2272
|
+
const factor = selectedOption?.toBaseFactor;
|
|
2273
|
+
if (!Number.isFinite(factor) || !factor || !baseOption) {
|
|
2274
|
+
return (
|
|
2275
|
+
<p className="text-xs text-muted-foreground">
|
|
2276
|
+
{t(
|
|
2277
|
+
"sales.documents.items.uomPreviewUnavailable",
|
|
2278
|
+
"Missing conversion to base unit.",
|
|
2279
|
+
)}
|
|
2280
|
+
</p>
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
const normalized = normalizeQuantityPreview(quantity * factor);
|
|
2284
|
+
return (
|
|
2285
|
+
<p className="text-xs text-muted-foreground">
|
|
2286
|
+
{t(
|
|
2287
|
+
"sales.documents.items.uomPreviewTemplate",
|
|
2288
|
+
"{{quantity}} {{unit}} → {{normalized}} {{baseUnit}}",
|
|
2289
|
+
{
|
|
2290
|
+
quantity,
|
|
2291
|
+
unit: enteredUnit,
|
|
2292
|
+
normalized,
|
|
2293
|
+
baseUnit: baseOption.code,
|
|
2294
|
+
},
|
|
2295
|
+
)}
|
|
2296
|
+
</p>
|
|
2297
|
+
);
|
|
2298
|
+
},
|
|
2299
|
+
} satisfies CrudField,
|
|
2300
|
+
{
|
|
2301
|
+
id: "statusEntryId",
|
|
2302
|
+
label: t("sales.documents.items.status", "Status"),
|
|
2303
|
+
type: "custom",
|
|
2304
|
+
layout: "half",
|
|
1260
2305
|
component: ({ value, setValue }: FieldRenderProps) => (
|
|
1261
2306
|
<LookupSelect
|
|
1262
|
-
value={typeof value ===
|
|
2307
|
+
value={typeof value === "string" ? value : null}
|
|
1263
2308
|
onChange={(next) => setValue(next ?? null)}
|
|
1264
|
-
placeholder={t(
|
|
1265
|
-
|
|
2309
|
+
placeholder={t(
|
|
2310
|
+
"sales.documents.items.statusPlaceholder",
|
|
2311
|
+
"Select status",
|
|
2312
|
+
)}
|
|
2313
|
+
emptyLabel={t("sales.documents.items.statusEmpty", "No status")}
|
|
1266
2314
|
fetchItems={fetchLineStatusItems}
|
|
1267
|
-
loadingLabel={t(
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
2315
|
+
loadingLabel={t(
|
|
2316
|
+
"sales.documents.items.statusLoading",
|
|
2317
|
+
"Loading statuses…",
|
|
2318
|
+
)}
|
|
2319
|
+
selectLabel={t("ui.lookupSelect.select", "Select")}
|
|
2320
|
+
selectedLabel={t("ui.lookupSelect.selected", "Selected")}
|
|
2321
|
+
clearLabel={t("ui.lookupSelect.clearSelection", "Clear selection")}
|
|
2322
|
+
startTypingLabel={t(
|
|
2323
|
+
"ui.lookupSelect.startTyping",
|
|
2324
|
+
"Start typing to search.",
|
|
2325
|
+
)}
|
|
1272
2326
|
minQuery={0}
|
|
1273
2327
|
/>
|
|
1274
2328
|
),
|
|
1275
2329
|
} satisfies CrudField,
|
|
1276
2330
|
{
|
|
1277
|
-
id:
|
|
1278
|
-
label: t(
|
|
1279
|
-
type:
|
|
1280
|
-
placeholder: t(
|
|
1281
|
-
|
|
2331
|
+
id: "name",
|
|
2332
|
+
label: t("sales.documents.items.name", "Name"),
|
|
2333
|
+
type: "text",
|
|
2334
|
+
placeholder: t(
|
|
2335
|
+
"sales.documents.items.namePlaceholder",
|
|
2336
|
+
"Optional line name",
|
|
2337
|
+
),
|
|
2338
|
+
layout: "full",
|
|
1282
2339
|
required: isCustomLine,
|
|
1283
2340
|
} satisfies CrudField,
|
|
1284
|
-
]
|
|
2341
|
+
];
|
|
1285
2342
|
}, [
|
|
2343
|
+
applyPriceSelection,
|
|
2344
|
+
convertUnitPriceForUnitChange,
|
|
1286
2345
|
currencyCode,
|
|
1287
2346
|
findTaxRateIdByValue,
|
|
1288
2347
|
loadPrices,
|
|
2348
|
+
loadProductUnits,
|
|
1289
2349
|
loadProductOptions,
|
|
1290
2350
|
loadVariantOptions,
|
|
1291
2351
|
fetchLineStatusItems,
|
|
1292
2352
|
priceLoading,
|
|
1293
2353
|
priceOptions,
|
|
1294
2354
|
productOption,
|
|
2355
|
+
unitOptions,
|
|
1295
2356
|
lineMode,
|
|
1296
2357
|
variantOption,
|
|
1297
2358
|
t,
|
|
1298
2359
|
taxRateMap,
|
|
1299
|
-
taxRates
|
|
2360
|
+
taxRates,
|
|
1300
2361
|
resolveTaxSelection,
|
|
2362
|
+
selectPriceAfterRefresh,
|
|
1301
2363
|
hasTaxMetadata,
|
|
1302
|
-
])
|
|
2364
|
+
]);
|
|
1303
2365
|
|
|
1304
2366
|
const groups = React.useMemo<CrudFormGroup[]>(() => {
|
|
1305
2367
|
return [
|
|
1306
|
-
{ id:
|
|
2368
|
+
{ id: "line-core", fields },
|
|
1307
2369
|
{
|
|
1308
|
-
id:
|
|
2370
|
+
id: "line-custom",
|
|
1309
2371
|
column: 2,
|
|
1310
|
-
title: t(
|
|
1311
|
-
kind:
|
|
2372
|
+
title: t("entities.customFields.title", "Custom fields"),
|
|
2373
|
+
kind: "customFields",
|
|
1312
2374
|
},
|
|
1313
|
-
]
|
|
1314
|
-
}, [fields, t])
|
|
2375
|
+
];
|
|
2376
|
+
}, [fields, t]);
|
|
1315
2377
|
|
|
1316
2378
|
React.useEffect(() => {
|
|
1317
|
-
if (!open) return
|
|
2379
|
+
if (!open) return;
|
|
1318
2380
|
if (!initialLine) {
|
|
1319
|
-
resetForm()
|
|
1320
|
-
return
|
|
2381
|
+
resetForm();
|
|
2382
|
+
return;
|
|
1321
2383
|
}
|
|
1322
|
-
setEditingId(initialLine.id)
|
|
1323
|
-
const nextForm = defaultForm(initialLine.currencyCode ?? currencyCode)
|
|
1324
|
-
const meta = initialLine.metadata ?? {}
|
|
1325
|
-
const snapshot =
|
|
2384
|
+
setEditingId(initialLine.id);
|
|
2385
|
+
const nextForm = defaultForm(initialLine.currencyCode ?? currencyCode);
|
|
2386
|
+
const meta = initialLine.metadata ?? {};
|
|
2387
|
+
const snapshot =
|
|
2388
|
+
(initialLine.catalogSnapshot as
|
|
2389
|
+
| Record<string, unknown>
|
|
2390
|
+
| null
|
|
2391
|
+
| undefined) ?? null;
|
|
1326
2392
|
const snapshotProduct =
|
|
1327
|
-
snapshot &&
|
|
1328
|
-
|
|
1329
|
-
|
|
2393
|
+
snapshot &&
|
|
2394
|
+
typeof snapshot === "object" &&
|
|
2395
|
+
typeof (snapshot as CatalogSnapshotRecord).product === "object" &&
|
|
2396
|
+
(snapshot as CatalogSnapshotRecord).product
|
|
2397
|
+
? ((snapshot as CatalogSnapshotRecord).product as Record<string, unknown>)
|
|
2398
|
+
: null;
|
|
1330
2399
|
const snapshotVariant =
|
|
1331
|
-
snapshot &&
|
|
1332
|
-
|
|
1333
|
-
|
|
2400
|
+
snapshot &&
|
|
2401
|
+
typeof snapshot === "object" &&
|
|
2402
|
+
typeof (snapshot as CatalogSnapshotRecord).variant === "object" &&
|
|
2403
|
+
(snapshot as CatalogSnapshotRecord).variant
|
|
2404
|
+
? ((snapshot as CatalogSnapshotRecord).variant as Record<string, unknown>)
|
|
2405
|
+
: null;
|
|
2406
|
+
const metaRec = (typeof meta === "object" && meta ? meta : null) as LineMetadataRecord | null;
|
|
1334
2407
|
const metaLineMode =
|
|
1335
|
-
typeof
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
nextForm.
|
|
1342
|
-
nextForm.
|
|
1343
|
-
|
|
2408
|
+
typeof metaRec?.lineMode === "string" &&
|
|
2409
|
+
(metaRec.lineMode === "custom" || metaRec.lineMode === "catalog")
|
|
2410
|
+
? (metaRec.lineMode as "custom" | "catalog")
|
|
2411
|
+
: metaRec?.customLine
|
|
2412
|
+
? "custom"
|
|
2413
|
+
: undefined;
|
|
2414
|
+
nextForm.productId = initialLine.productId;
|
|
2415
|
+
nextForm.variantId = initialLine.productVariantId;
|
|
2416
|
+
nextForm.quantity = initialLine.quantity.toString();
|
|
2417
|
+
nextForm.quantityUnit = normalizeUnitCode(initialLine.quantityUnit) ?? null;
|
|
2418
|
+
const metaMode = metaRec?.priceMode;
|
|
1344
2419
|
const resolvedPriceMode =
|
|
1345
|
-
metaMode ===
|
|
2420
|
+
metaMode === "net" || metaMode === "gross"
|
|
2421
|
+
? metaMode
|
|
2422
|
+
: (initialLine.priceMode ?? "gross");
|
|
1346
2423
|
nextForm.unitPrice =
|
|
1347
|
-
resolvedPriceMode ===
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
nextForm.
|
|
1351
|
-
nextForm.
|
|
1352
|
-
|
|
1353
|
-
|
|
2424
|
+
resolvedPriceMode === "net"
|
|
2425
|
+
? initialLine.unitPriceNet.toString()
|
|
2426
|
+
: initialLine.unitPriceGross.toString();
|
|
2427
|
+
nextForm.priceMode = resolvedPriceMode;
|
|
2428
|
+
nextForm.taxRate = Number.isFinite(initialLine.taxRate)
|
|
2429
|
+
? initialLine.taxRate
|
|
2430
|
+
: null;
|
|
2431
|
+
nextForm.name = initialLine.name ?? "";
|
|
2432
|
+
nextForm.catalogSnapshot = snapshot ?? null;
|
|
2433
|
+
nextForm.customFieldSetId = initialLine.customFieldSetId ?? null;
|
|
2434
|
+
nextForm.statusEntryId = initialLine.statusEntryId ?? null;
|
|
1354
2435
|
nextForm.lineMode =
|
|
1355
2436
|
metaLineMode ??
|
|
1356
|
-
(initialLine.productId || initialLine.productVariantId
|
|
2437
|
+
(initialLine.productId || initialLine.productVariantId
|
|
2438
|
+
? "catalog"
|
|
2439
|
+
: "custom");
|
|
1357
2440
|
const metaTaxRateId =
|
|
1358
|
-
typeof
|
|
1359
|
-
|
|
2441
|
+
typeof metaRec?.taxRateId === "string"
|
|
2442
|
+
? metaRec.taxRateId
|
|
2443
|
+
: null;
|
|
2444
|
+
const fallbackTaxRateId = findTaxRateIdByValue(nextForm.taxRate);
|
|
1360
2445
|
nextForm.taxRateId =
|
|
1361
2446
|
metaTaxRateId ??
|
|
1362
2447
|
fallbackTaxRateId ??
|
|
1363
|
-
(defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null)
|
|
2448
|
+
(defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null);
|
|
1364
2449
|
if (!Number.isFinite(nextForm.taxRate) && nextForm.taxRateId) {
|
|
1365
|
-
const matched = taxRatesRef.current.find(
|
|
1366
|
-
|
|
2450
|
+
const matched = taxRatesRef.current.find(
|
|
2451
|
+
(rate) => rate.id === nextForm.taxRateId,
|
|
2452
|
+
);
|
|
2453
|
+
const numericRate = normalizeNumber(matched?.rate);
|
|
1367
2454
|
if (Number.isFinite(numericRate)) {
|
|
1368
|
-
nextForm.taxRate = numericRate
|
|
2455
|
+
nextForm.taxRate = numericRate;
|
|
1369
2456
|
}
|
|
1370
2457
|
}
|
|
1371
|
-
let resolvedProductOption: ProductOption | null = null
|
|
1372
|
-
let resolvedVariantOption: VariantOption | null = null
|
|
1373
|
-
if (
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
2458
|
+
let resolvedProductOption: ProductOption | null = null;
|
|
2459
|
+
let resolvedVariantOption: VariantOption | null = null;
|
|
2460
|
+
if (metaRec) {
|
|
2461
|
+
const metaRecord = metaRec;
|
|
2462
|
+
const mode = metaRecord.priceMode;
|
|
2463
|
+
if (mode === "net" || mode === "gross") {
|
|
2464
|
+
nextForm.priceMode = mode;
|
|
1377
2465
|
nextForm.unitPrice =
|
|
1378
|
-
mode ===
|
|
2466
|
+
mode === "net"
|
|
2467
|
+
? initialLine.unitPriceNet.toString()
|
|
2468
|
+
: initialLine.unitPriceGross.toString();
|
|
1379
2469
|
}
|
|
1380
2470
|
nextForm.priceId =
|
|
1381
|
-
typeof
|
|
1382
|
-
|
|
1383
|
-
|
|
2471
|
+
typeof metaRecord.priceId === "string"
|
|
2472
|
+
? metaRecord.priceId
|
|
2473
|
+
: null;
|
|
2474
|
+
const productTitle =
|
|
2475
|
+
typeof metaRecord.productTitle === "string"
|
|
2476
|
+
? metaRecord.productTitle
|
|
2477
|
+
: initialLine.name;
|
|
2478
|
+
const productSku =
|
|
2479
|
+
typeof metaRecord.productSku === "string"
|
|
2480
|
+
? metaRecord.productSku
|
|
2481
|
+
: null;
|
|
1384
2482
|
const productThumbnail =
|
|
1385
|
-
typeof
|
|
2483
|
+
typeof metaRecord.productThumbnail === "string"
|
|
2484
|
+
? metaRecord.productThumbnail
|
|
2485
|
+
: null;
|
|
1386
2486
|
if (productTitle && initialLine.productId) {
|
|
1387
|
-
const option = {
|
|
1388
|
-
|
|
1389
|
-
|
|
2487
|
+
const option = {
|
|
2488
|
+
id: initialLine.productId,
|
|
2489
|
+
title: productTitle,
|
|
2490
|
+
sku: productSku,
|
|
2491
|
+
thumbnailUrl: productThumbnail,
|
|
2492
|
+
};
|
|
2493
|
+
productOptionsRef.current.set(initialLine.productId, option);
|
|
2494
|
+
resolvedProductOption = option;
|
|
1390
2495
|
}
|
|
1391
|
-
const variantTitle =
|
|
1392
|
-
|
|
2496
|
+
const variantTitle =
|
|
2497
|
+
typeof metaRecord.variantTitle === "string"
|
|
2498
|
+
? metaRecord.variantTitle
|
|
2499
|
+
: null;
|
|
2500
|
+
const variantSku =
|
|
2501
|
+
typeof metaRecord.variantSku === "string"
|
|
2502
|
+
? metaRecord.variantSku
|
|
2503
|
+
: null;
|
|
1393
2504
|
const variantThumb =
|
|
1394
|
-
typeof
|
|
2505
|
+
typeof metaRecord.variantThumbnail === "string"
|
|
2506
|
+
? metaRecord.variantThumbnail
|
|
2507
|
+
: productThumbnail;
|
|
1395
2508
|
if (variantTitle && initialLine.productVariantId) {
|
|
1396
2509
|
const option = {
|
|
1397
2510
|
id: initialLine.productVariantId,
|
|
1398
2511
|
title: variantTitle,
|
|
1399
2512
|
sku: variantSku,
|
|
1400
2513
|
thumbnailUrl: variantThumb ?? null,
|
|
1401
|
-
}
|
|
1402
|
-
variantOptionsRef.current.set(initialLine.productVariantId, option)
|
|
1403
|
-
resolvedVariantOption = option
|
|
2514
|
+
};
|
|
2515
|
+
variantOptionsRef.current.set(initialLine.productVariantId, option);
|
|
2516
|
+
resolvedVariantOption = option;
|
|
1404
2517
|
}
|
|
1405
2518
|
}
|
|
1406
2519
|
if (!resolvedProductOption && initialLine.productId && snapshotProduct) {
|
|
2520
|
+
const sp = snapshotProduct as SnapshotEntity;
|
|
1407
2521
|
const snapshotTitle =
|
|
1408
|
-
typeof
|
|
1409
|
-
?
|
|
1410
|
-
: initialLine.name ?? initialLine.productId
|
|
2522
|
+
typeof sp.title === "string" && sp.title.trim().length
|
|
2523
|
+
? sp.title
|
|
2524
|
+
: (initialLine.name ?? initialLine.productId);
|
|
1411
2525
|
const snapshotSku =
|
|
1412
|
-
typeof
|
|
1413
|
-
?
|
|
1414
|
-
: null
|
|
2526
|
+
typeof sp.sku === "string" && sp.sku.trim().length
|
|
2527
|
+
? sp.sku
|
|
2528
|
+
: null;
|
|
1415
2529
|
const snapshotThumb =
|
|
1416
|
-
typeof
|
|
1417
|
-
?
|
|
1418
|
-
: typeof
|
|
1419
|
-
?
|
|
1420
|
-
: null
|
|
1421
|
-
const snapshotTaxRate = normalizeNumber(
|
|
1422
|
-
const option = {
|
|
2530
|
+
typeof sp.thumbnailUrl === "string"
|
|
2531
|
+
? sp.thumbnailUrl
|
|
2532
|
+
: typeof sp.thumbnail_url === "string"
|
|
2533
|
+
? sp.thumbnail_url
|
|
2534
|
+
: null;
|
|
2535
|
+
const snapshotTaxRate = normalizeNumber(sp.taxRate, Number.NaN);
|
|
2536
|
+
const option: ProductOption = {
|
|
1423
2537
|
id: initialLine.productId,
|
|
1424
2538
|
title: snapshotTitle,
|
|
1425
2539
|
sku: snapshotSku,
|
|
1426
2540
|
thumbnailUrl: snapshotThumb,
|
|
1427
|
-
taxRateId: typeof
|
|
2541
|
+
taxRateId: typeof sp.taxRateId === "string" ? sp.taxRateId : null,
|
|
1428
2542
|
taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null,
|
|
1429
|
-
}
|
|
1430
|
-
productOptionsRef.current.set(initialLine.productId, option)
|
|
1431
|
-
resolvedProductOption = option
|
|
2543
|
+
};
|
|
2544
|
+
productOptionsRef.current.set(initialLine.productId, option);
|
|
2545
|
+
resolvedProductOption = option;
|
|
1432
2546
|
}
|
|
1433
|
-
if (
|
|
2547
|
+
if (
|
|
2548
|
+
!resolvedVariantOption &&
|
|
2549
|
+
initialLine.productVariantId &&
|
|
2550
|
+
snapshotVariant
|
|
2551
|
+
) {
|
|
2552
|
+
const sv = snapshotVariant as SnapshotEntity;
|
|
1434
2553
|
const snapshotTitle =
|
|
1435
|
-
typeof
|
|
1436
|
-
?
|
|
1437
|
-
: initialLine.name ?? initialLine.productVariantId
|
|
2554
|
+
typeof sv.title === "string" && sv.title.trim().length
|
|
2555
|
+
? sv.title
|
|
2556
|
+
: (initialLine.name ?? initialLine.productVariantId);
|
|
1438
2557
|
const snapshotSku =
|
|
1439
|
-
typeof
|
|
1440
|
-
?
|
|
1441
|
-
: null
|
|
2558
|
+
typeof sv.sku === "string" && sv.sku.trim().length
|
|
2559
|
+
? sv.sku
|
|
2560
|
+
: null;
|
|
1442
2561
|
const snapshotThumb =
|
|
1443
|
-
typeof
|
|
1444
|
-
?
|
|
1445
|
-
: typeof
|
|
1446
|
-
?
|
|
1447
|
-
: resolvedProductOption?.thumbnailUrl ??
|
|
1448
|
-
|
|
1449
|
-
|
|
2562
|
+
typeof sv.thumbnailUrl === "string"
|
|
2563
|
+
? sv.thumbnailUrl
|
|
2564
|
+
: typeof sv.thumbnail_url === "string"
|
|
2565
|
+
? sv.thumbnail_url
|
|
2566
|
+
: (resolvedProductOption?.thumbnailUrl ??
|
|
2567
|
+
productOptionsRef.current.get(initialLine.productId ?? "")
|
|
2568
|
+
?.thumbnailUrl ??
|
|
2569
|
+
null);
|
|
2570
|
+
const snapshotTaxRate = normalizeNumber(sv.taxRate, Number.NaN);
|
|
2571
|
+
const option: VariantOption = {
|
|
1450
2572
|
id: initialLine.productVariantId,
|
|
1451
2573
|
title: snapshotTitle,
|
|
1452
2574
|
sku: snapshotSku,
|
|
1453
2575
|
thumbnailUrl: snapshotThumb,
|
|
1454
|
-
taxRateId: typeof
|
|
2576
|
+
taxRateId: typeof sv.taxRateId === "string" ? sv.taxRateId : null,
|
|
1455
2577
|
taxRate: Number.isFinite(snapshotTaxRate) ? snapshotTaxRate : null,
|
|
1456
|
-
}
|
|
1457
|
-
variantOptionsRef.current.set(initialLine.productVariantId, option)
|
|
1458
|
-
resolvedVariantOption = option
|
|
2578
|
+
};
|
|
2579
|
+
variantOptionsRef.current.set(initialLine.productVariantId, option);
|
|
2580
|
+
resolvedVariantOption = option;
|
|
1459
2581
|
}
|
|
1460
|
-
if (resolvedProductOption) setProductOption(resolvedProductOption)
|
|
1461
|
-
if (resolvedVariantOption) setVariantOption(resolvedVariantOption)
|
|
1462
|
-
const customValues = extractCustomFieldValues(
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2582
|
+
if (resolvedProductOption) setProductOption(resolvedProductOption);
|
|
2583
|
+
if (resolvedVariantOption) setVariantOption(resolvedVariantOption);
|
|
2584
|
+
const customValues = extractCustomFieldValues(
|
|
2585
|
+
initialLine as Record<string, unknown>,
|
|
2586
|
+
);
|
|
2587
|
+
const merged = { ...nextForm, ...customValues };
|
|
2588
|
+
setInitialValues(merged);
|
|
2589
|
+
setLineMode(merged.lineMode);
|
|
2590
|
+
setFormResetKey((prev) => prev + 1);
|
|
1467
2591
|
if (initialLine.productId) {
|
|
1468
|
-
void
|
|
2592
|
+
void loadProductUnits(initialLine.productId, resolvedProductOption).then(
|
|
2593
|
+
(options) => {
|
|
2594
|
+
const requestedUnit = normalizeUnitCode(nextForm.quantityUnit);
|
|
2595
|
+
if (
|
|
2596
|
+
requestedUnit &&
|
|
2597
|
+
!options.some((entry) => entry.code === requestedUnit)
|
|
2598
|
+
) {
|
|
2599
|
+
setUnitOptions((prev) => [
|
|
2600
|
+
...prev,
|
|
2601
|
+
{ code: requestedUnit, toBaseFactor: null, isBase: false },
|
|
2602
|
+
]);
|
|
2603
|
+
}
|
|
2604
|
+
},
|
|
2605
|
+
);
|
|
2606
|
+
void loadPrices(
|
|
2607
|
+
initialLine.productId,
|
|
2608
|
+
initialLine.productVariantId,
|
|
2609
|
+
nextForm.quantity,
|
|
2610
|
+
nextForm.quantityUnit,
|
|
2611
|
+
);
|
|
1469
2612
|
} else {
|
|
1470
|
-
setPriceOptions([])
|
|
2613
|
+
setPriceOptions([]);
|
|
2614
|
+
setUnitOptions([]);
|
|
1471
2615
|
}
|
|
1472
|
-
}, [
|
|
2616
|
+
}, [
|
|
2617
|
+
currencyCode,
|
|
2618
|
+
findTaxRateIdByValue,
|
|
2619
|
+
initialLine,
|
|
2620
|
+
loadPrices,
|
|
2621
|
+
loadProductUnits,
|
|
2622
|
+
open,
|
|
2623
|
+
resetForm,
|
|
2624
|
+
]);
|
|
1473
2625
|
|
|
1474
2626
|
return (
|
|
1475
|
-
<Dialog
|
|
2627
|
+
<Dialog
|
|
2628
|
+
open={open}
|
|
2629
|
+
onOpenChange={(next) => (next ? onOpenChange(true) : closeDialog())}
|
|
2630
|
+
>
|
|
1476
2631
|
<DialogContent
|
|
1477
2632
|
className="sm:max-w-5xl"
|
|
1478
2633
|
ref={dialogContentRef}
|
|
1479
2634
|
onKeyDown={(event) => {
|
|
1480
|
-
if ((event.metaKey || event.ctrlKey) && event.key ===
|
|
1481
|
-
event.preventDefault()
|
|
1482
|
-
dialogContentRef.current?.querySelector(
|
|
2635
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
2636
|
+
event.preventDefault();
|
|
2637
|
+
dialogContentRef.current?.querySelector("form")?.requestSubmit();
|
|
1483
2638
|
}
|
|
1484
|
-
if (event.key ===
|
|
1485
|
-
event.preventDefault()
|
|
1486
|
-
closeDialog()
|
|
2639
|
+
if (event.key === "Escape") {
|
|
2640
|
+
event.preventDefault();
|
|
2641
|
+
closeDialog();
|
|
1487
2642
|
}
|
|
1488
2643
|
}}
|
|
1489
2644
|
>
|
|
1490
2645
|
<DialogHeader>
|
|
1491
2646
|
<DialogTitle>
|
|
1492
2647
|
{editingId
|
|
1493
|
-
? t(
|
|
1494
|
-
: t(
|
|
2648
|
+
? t("sales.documents.items.editTitle", "Edit line")
|
|
2649
|
+
: t("sales.documents.items.addTitle", "Add line")}
|
|
1495
2650
|
</DialogTitle>
|
|
1496
2651
|
</DialogHeader>
|
|
1497
2652
|
<CrudForm<LineFormState>
|
|
@@ -1503,13 +2658,13 @@ export function LineItemDialog({
|
|
|
1503
2658
|
initialValues={initialValues}
|
|
1504
2659
|
submitLabel={
|
|
1505
2660
|
editingId
|
|
1506
|
-
? t(
|
|
1507
|
-
: t(
|
|
2661
|
+
? t("sales.documents.items.save", "Save changes")
|
|
2662
|
+
: t("sales.documents.items.addLine", "Add item")
|
|
1508
2663
|
}
|
|
1509
2664
|
onSubmit={handleFormSubmit}
|
|
1510
|
-
loadingMessage={t(
|
|
2665
|
+
loadingMessage={t("sales.documents.items.loading", "Loading items…")}
|
|
1511
2666
|
/>
|
|
1512
2667
|
</DialogContent>
|
|
1513
2668
|
</Dialog>
|
|
1514
|
-
)
|
|
2669
|
+
);
|
|
1515
2670
|
}
|