@open-mercato/core 0.6.8-develop.7057.1.61440fc3bc → 0.6.8-develop.7063.1.664341cf65
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/acl.js +30 -5
- package/dist/modules/auth/acl.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +1 -1
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +1 -1
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +37 -15
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +76 -12
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +1 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +1 -1
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js +54 -0
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/acl.ts +30 -6
- package/src/modules/customers/backend/customers/companies/page.tsx +1 -1
- package/src/modules/customers/backend/customers/people/page.tsx +1 -1
- package/src/modules/sales/components/documents/ItemsSection.tsx +48 -17
- package/src/modules/sales/components/documents/LineItemDialog.tsx +129 -13
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +1 -0
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +1 -1
- package/src/modules/sales/components/documents/lineItemShipmentLock.ts +74 -0
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/ko.json +3 -0
- package/src/modules/sales/i18n/pl.json +3 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sales/components/documents/SalesOrderDraftLines.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Plus } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LineItemDialog } from './LineItemDialog'\nimport type { SalesLineRecord } from './lineItemTypes'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\n\nexport type SalesOrderLineDraft = {\n id: string\n payload: Record<string, unknown>\n record: SalesLineRecord\n}\n\ntype SalesOrderDraftLinesProps = {\n currencyCode: string | null | undefined\n organizationId: string | null\n tenantId: string | null\n lines: SalesOrderLineDraft[]\n error?: string | null\n onChange: (lines: SalesOrderLineDraft[]) => void\n}\n\nfunction draftId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `draft-${Date.now()}-${Math.round(performance.now())}`\n}\n\nexport function createSalesOrderLineDraft(\n payload: Record<string, unknown>,\n id = draftId(),\n): SalesOrderLineDraft {\n const quantity = normalizeNumber(payload.quantity, 0)\n const unitPriceNet = normalizeNumber(payload.unitPriceNet, 0)\n const unitPriceGross = normalizeNumber(payload.unitPriceGross, unitPriceNet)\n const taxRate = normalizeNumber(payload.taxRate, 0)\n const totalNet = normalizeNumber(payload.totalNetAmount, unitPriceNet * quantity)\n const totalGross = normalizeNumber(payload.totalGrossAmount, unitPriceGross * quantity)\n const metadata = payload.metadata && typeof payload.metadata === 'object'\n ? payload.metadata as Record<string, unknown>\n : null\n const catalogSnapshot = payload.catalogSnapshot && typeof payload.catalogSnapshot === 'object'\n ? payload.catalogSnapshot as Record<string, unknown>\n : null\n\n return {\n id,\n payload: { ...payload },\n record: {\n id,\n name: typeof payload.name === 'string' ? payload.name : null,\n productId: typeof payload.productId === 'string' ? payload.productId : null,\n productVariantId: typeof payload.productVariantId === 'string' ? payload.productVariantId : null,\n quantity,\n quantityUnit: typeof payload.quantityUnit === 'string' ? payload.quantityUnit : null,\n normalizedQuantity: normalizeNumber(payload.normalizedQuantity, quantity),\n normalizedUnit: typeof payload.normalizedUnit === 'string' ? payload.normalizedUnit : null,\n currencyCode: typeof payload.currencyCode === 'string' ? payload.currencyCode : null,\n unitPriceNet,\n unitPriceGross,\n discountAmount: normalizeNumber(payload.discountAmount, 0) * quantity,\n discountPercent: normalizeNumber(payload.discountPercent, 0),\n taxRate,\n totalNet,\n totalGross,\n priceMode: payload.priceMode === 'net' ? 'net' : 'gross',\n uomSnapshot: null,\n metadata,\n catalogSnapshot,\n customFieldSetId: typeof payload.customFieldSetId === 'string' ? payload.customFieldSetId : null,\n customFields: payload.customFields && typeof payload.customFields === 'object'\n ? payload.customFields as Record<string, unknown>\n : null,\n status: null,\n statusEntryId: typeof payload.statusEntryId === 'string' ? payload.statusEntryId : null,\n },\n }\n}\n\nexport function SalesOrderDraftLines({\n currencyCode,\n organizationId,\n tenantId,\n lines,\n error,\n onChange,\n}: SalesOrderDraftLinesProps) {\n const t = useT()\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editing, setEditing] = React.useState<SalesOrderLineDraft | null>(null)\n\n const columns = React.useMemo<ColumnDef<SalesOrderLineDraft>[]>(() => [\n {\n id: 'name',\n header: t('sales.documents.items.table.product', 'Product'),\n cell: ({ row }) => row.original.record.name ?? t('sales.documents.items.untitled', 'Untitled'),\n },\n {\n id: 'quantity',\n header: t('sales.documents.items.table.quantity', 'Qty'),\n cell: ({ row }) => row.original.record.quantityUnit\n ? `${row.original.record.quantity} ${row.original.record.quantityUnit}`\n : row.original.record.quantity,\n },\n {\n id: 'unitPrice',\n header: t('sales.documents.items.table.unit', 'Unit price'),\n cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n {\n id: 'total',\n header: t('sales.documents.items.table.total', 'Total'),\n cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n ], [currencyCode, t])\n\n const openCreate = React.useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n\n const rowActions = React.useCallback((line: SalesOrderLineDraft) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => {\n setEditing(line)\n setDialogOpen(true)\n },\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => onChange(lines.filter((candidate) => candidate.id !== line.id)),\n },\n ]}\n />\n ), [lines, onChange, t])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('sales.orders.form.lines', 'Line Items')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={openCreate}>\n <Plus className=\"h-4 w-4\" aria-hidden=\"true\" />\n {t('sales.documents.items.add', 'Add item')}\n </Button>\n </div>\n {error ? <p className=\"text-sm text-
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Plus } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { LineItemDialog } from './LineItemDialog'\nimport type { SalesLineRecord } from './lineItemTypes'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\n\nexport type SalesOrderLineDraft = {\n id: string\n payload: Record<string, unknown>\n record: SalesLineRecord\n}\n\ntype SalesOrderDraftLinesProps = {\n currencyCode: string | null | undefined\n organizationId: string | null\n tenantId: string | null\n lines: SalesOrderLineDraft[]\n error?: string | null\n onChange: (lines: SalesOrderLineDraft[]) => void\n}\n\nfunction draftId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `draft-${Date.now()}-${Math.round(performance.now())}`\n}\n\nexport function createSalesOrderLineDraft(\n payload: Record<string, unknown>,\n id = draftId(),\n): SalesOrderLineDraft {\n const quantity = normalizeNumber(payload.quantity, 0)\n const unitPriceNet = normalizeNumber(payload.unitPriceNet, 0)\n const unitPriceGross = normalizeNumber(payload.unitPriceGross, unitPriceNet)\n const taxRate = normalizeNumber(payload.taxRate, 0)\n const totalNet = normalizeNumber(payload.totalNetAmount, unitPriceNet * quantity)\n const totalGross = normalizeNumber(payload.totalGrossAmount, unitPriceGross * quantity)\n const metadata = payload.metadata && typeof payload.metadata === 'object'\n ? payload.metadata as Record<string, unknown>\n : null\n const catalogSnapshot = payload.catalogSnapshot && typeof payload.catalogSnapshot === 'object'\n ? payload.catalogSnapshot as Record<string, unknown>\n : null\n\n return {\n id,\n payload: { ...payload },\n record: {\n id,\n name: typeof payload.name === 'string' ? payload.name : null,\n productId: typeof payload.productId === 'string' ? payload.productId : null,\n productVariantId: typeof payload.productVariantId === 'string' ? payload.productVariantId : null,\n quantity,\n quantityUnit: typeof payload.quantityUnit === 'string' ? payload.quantityUnit : null,\n normalizedQuantity: normalizeNumber(payload.normalizedQuantity, quantity),\n normalizedUnit: typeof payload.normalizedUnit === 'string' ? payload.normalizedUnit : null,\n currencyCode: typeof payload.currencyCode === 'string' ? payload.currencyCode : null,\n unitPriceNet,\n unitPriceGross,\n discountAmount: normalizeNumber(payload.discountAmount, 0) * quantity,\n discountPercent: normalizeNumber(payload.discountPercent, 0),\n taxRate,\n totalNet,\n totalGross,\n priceMode: payload.priceMode === 'net' ? 'net' : 'gross',\n uomSnapshot: null,\n metadata,\n catalogSnapshot,\n customFieldSetId: typeof payload.customFieldSetId === 'string' ? payload.customFieldSetId : null,\n customFields: payload.customFields && typeof payload.customFields === 'object'\n ? payload.customFields as Record<string, unknown>\n : null,\n status: null,\n statusEntryId: typeof payload.statusEntryId === 'string' ? payload.statusEntryId : null,\n },\n }\n}\n\nexport function SalesOrderDraftLines({\n currencyCode,\n organizationId,\n tenantId,\n lines,\n error,\n onChange,\n}: SalesOrderDraftLinesProps) {\n const t = useT()\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editing, setEditing] = React.useState<SalesOrderLineDraft | null>(null)\n\n const columns = React.useMemo<ColumnDef<SalesOrderLineDraft>[]>(() => [\n {\n id: 'name',\n header: t('sales.documents.items.table.product', 'Product'),\n cell: ({ row }) => row.original.record.name ?? t('sales.documents.items.untitled', 'Untitled'),\n },\n {\n id: 'quantity',\n header: t('sales.documents.items.table.quantity', 'Qty'),\n cell: ({ row }) => row.original.record.quantityUnit\n ? `${row.original.record.quantity} ${row.original.record.quantityUnit}`\n : row.original.record.quantity,\n },\n {\n id: 'unitPrice',\n header: t('sales.documents.items.table.unit', 'Unit price'),\n cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n {\n id: 'total',\n header: t('sales.documents.items.table.total', 'Total'),\n cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined),\n },\n ], [currencyCode, t])\n\n const openCreate = React.useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n\n const rowActions = React.useCallback((line: SalesOrderLineDraft) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => {\n setEditing(line)\n setDialogOpen(true)\n },\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => onChange(lines.filter((candidate) => candidate.id !== line.id)),\n },\n ]}\n />\n ), [lines, onChange, t])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium\">{t('sales.orders.form.lines', 'Line Items')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={openCreate}>\n <Plus className=\"h-4 w-4\" aria-hidden=\"true\" />\n {t('sales.documents.items.add', 'Add item')}\n </Button>\n </div>\n {error ? <p className=\"text-sm text-status-error-text\" role=\"alert\">{error}</p> : null}\n <DataTable\n columns={columns}\n data={lines}\n embedded\n disableRowClick\n rowActions={rowActions}\n emptyState={(\n <EmptyState\n size=\"sm\"\n title={t('sales.documents.items.empty', 'No items yet.')}\n description={t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')}\n actions={<Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={openCreate}>{t('sales.documents.items.add', 'Add item')}</Button>}\n />\n )}\n />\n <LineItemDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n setDialogOpen(open)\n if (!open) setEditing(null)\n }}\n kind=\"order\"\n currencyCode={currencyCode}\n organizationId={organizationId}\n tenantId={tenantId}\n initialLine={editing?.record ?? null}\n onDraftSaved={(payload, lineId) => {\n const draft = createSalesOrderLineDraft(payload, lineId ?? undefined)\n onChange(lineId\n ? lines.map((line) => line.id === lineId ? draft : line)\n : [...lines, draft])\n }}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS = [
|
|
2
|
+
"kind",
|
|
3
|
+
"productId",
|
|
4
|
+
"productVariantId",
|
|
5
|
+
"quantityUnit",
|
|
6
|
+
"unitPriceNet",
|
|
7
|
+
"unitPriceGross",
|
|
8
|
+
"priceId",
|
|
9
|
+
"priceMode",
|
|
10
|
+
"taxRateId",
|
|
11
|
+
"taxRate",
|
|
12
|
+
"taxAmount",
|
|
13
|
+
"discountAmount",
|
|
14
|
+
"discountPercent",
|
|
15
|
+
"catalogSnapshot",
|
|
16
|
+
"metadata"
|
|
17
|
+
];
|
|
18
|
+
function scaleTotal(total, previousQuantity, nextQuantity) {
|
|
19
|
+
if (!Number.isFinite(total) || !Number.isFinite(previousQuantity) || previousQuantity <= 0) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
return total * (nextQuantity / previousQuantity);
|
|
23
|
+
}
|
|
24
|
+
function prepareShippedLineUpdatePayload(payload, currentLine) {
|
|
25
|
+
if (!currentLine) return payload;
|
|
26
|
+
const nextPayload = { ...payload };
|
|
27
|
+
for (const field of SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS) {
|
|
28
|
+
delete nextPayload[field];
|
|
29
|
+
}
|
|
30
|
+
delete nextPayload.totalNetAmount;
|
|
31
|
+
delete nextPayload.totalGrossAmount;
|
|
32
|
+
const nextQuantity = Number(nextPayload.quantity);
|
|
33
|
+
if (!Number.isFinite(nextQuantity) || nextQuantity === currentLine.quantity) {
|
|
34
|
+
return nextPayload;
|
|
35
|
+
}
|
|
36
|
+
const scaledNetTotal = scaleTotal(
|
|
37
|
+
currentLine.totalNetAmount,
|
|
38
|
+
currentLine.quantity,
|
|
39
|
+
nextQuantity
|
|
40
|
+
);
|
|
41
|
+
const scaledGrossTotal = scaleTotal(
|
|
42
|
+
currentLine.totalGrossAmount,
|
|
43
|
+
currentLine.quantity,
|
|
44
|
+
nextQuantity
|
|
45
|
+
);
|
|
46
|
+
if (scaledNetTotal !== void 0) nextPayload.totalNetAmount = scaledNetTotal;
|
|
47
|
+
if (scaledGrossTotal !== void 0)
|
|
48
|
+
nextPayload.totalGrossAmount = scaledGrossTotal;
|
|
49
|
+
return nextPayload;
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
prepareShippedLineUpdatePayload
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=lineItemShipmentLock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/sales/components/documents/lineItemShipmentLock.ts"],
|
|
4
|
+
"sourcesContent": ["type ShippedLineSnapshot = {\n quantity: number;\n totalNetAmount?: number | null;\n totalGrossAmount?: number | null;\n};\n\nconst SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS = [\n \"kind\",\n \"productId\",\n \"productVariantId\",\n \"quantityUnit\",\n \"unitPriceNet\",\n \"unitPriceGross\",\n \"priceId\",\n \"priceMode\",\n \"taxRateId\",\n \"taxRate\",\n \"taxAmount\",\n \"discountAmount\",\n \"discountPercent\",\n \"catalogSnapshot\",\n \"metadata\",\n] as const;\n\nfunction scaleTotal(\n total: number | null | undefined,\n previousQuantity: number,\n nextQuantity: number,\n): number | undefined {\n if (\n !Number.isFinite(total) ||\n !Number.isFinite(previousQuantity) ||\n previousQuantity <= 0\n ) {\n return undefined;\n }\n return (total as number) * (nextQuantity / previousQuantity);\n}\n\nexport function prepareShippedLineUpdatePayload(\n payload: Record<string, unknown>,\n currentLine: ShippedLineSnapshot | null,\n): Record<string, unknown> {\n if (!currentLine) return payload;\n\n const nextPayload = { ...payload };\n for (const field of SHIPPED_LINE_IMMUTABLE_PAYLOAD_FIELDS) {\n delete nextPayload[field];\n }\n\n delete nextPayload.totalNetAmount;\n delete nextPayload.totalGrossAmount;\n\n const nextQuantity = Number(nextPayload.quantity);\n if (!Number.isFinite(nextQuantity) || nextQuantity === currentLine.quantity) {\n return nextPayload;\n }\n\n const scaledNetTotal = scaleTotal(\n currentLine.totalNetAmount,\n currentLine.quantity,\n nextQuantity,\n );\n const scaledGrossTotal = scaleTotal(\n currentLine.totalGrossAmount,\n currentLine.quantity,\n nextQuantity,\n );\n if (scaledNetTotal !== undefined) nextPayload.totalNetAmount = scaledNetTotal;\n if (scaledGrossTotal !== undefined)\n nextPayload.totalGrossAmount = scaledGrossTotal;\n\n return nextPayload;\n}\n"],
|
|
5
|
+
"mappings": "AAMA,MAAM,wCAAwC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,WACP,OACA,kBACA,cACoB;AACpB,MACE,CAAC,OAAO,SAAS,KAAK,KACtB,CAAC,OAAO,SAAS,gBAAgB,KACjC,oBAAoB,GACpB;AACA,WAAO;AAAA,EACT;AACA,SAAQ,SAAoB,eAAe;AAC7C;AAEO,SAAS,gCACd,SACA,aACyB;AACzB,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,cAAc,EAAE,GAAG,QAAQ;AACjC,aAAW,SAAS,uCAAuC;AACzD,WAAO,YAAY,KAAK;AAAA,EAC1B;AAEA,SAAO,YAAY;AACnB,SAAO,YAAY;AAEnB,QAAM,eAAe,OAAO,YAAY,QAAQ;AAChD,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,iBAAiB,YAAY,UAAU;AAC3E,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB;AAAA,IACrB,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,mBAAmB;AAAA,IACvB,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,EACF;AACA,MAAI,mBAAmB,OAAW,aAAY,iBAAiB;AAC/D,MAAI,qBAAqB;AACvB,gBAAY,mBAAmB;AAEjC,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7063.1.664341cf65",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7063.1.664341cf65",
|
|
256
|
+
"@open-mercato/shared": "0.6.8-develop.7063.1.664341cf65",
|
|
257
|
+
"@open-mercato/ui": "0.6.8-develop.7063.1.664341cf65",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
263
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
264
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7063.1.664341cf65",
|
|
263
|
+
"@open-mercato/shared": "0.6.8-develop.7063.1.664341cf65",
|
|
264
|
+
"@open-mercato/ui": "0.6.8-develop.7063.1.664341cf65",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
package/src/modules/auth/acl.ts
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
// Module-level features declaration for RBAC
|
|
2
2
|
export const features = [
|
|
3
3
|
{ id: 'auth.users.list', title: 'List users', module: 'auth' },
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
{
|
|
5
|
+
id: 'auth.users.create',
|
|
6
|
+
title: 'Create users',
|
|
7
|
+
module: 'auth',
|
|
8
|
+
dependsOn: ['auth.users.list', 'auth.roles.list', 'directory.organizations.view'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: 'auth.users.edit',
|
|
12
|
+
title: 'Edit users',
|
|
13
|
+
module: 'auth',
|
|
14
|
+
dependsOn: ['auth.users.list', 'auth.roles.list'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'auth.users.delete',
|
|
18
|
+
title: 'Delete users',
|
|
19
|
+
module: 'auth',
|
|
20
|
+
dependsOn: ['auth.users.list'],
|
|
21
|
+
},
|
|
7
22
|
{ id: 'auth.roles.list', title: 'List roles', module: 'auth' },
|
|
8
|
-
{
|
|
9
|
-
|
|
23
|
+
{
|
|
24
|
+
id: 'auth.roles.manage',
|
|
25
|
+
title: 'Manage roles',
|
|
26
|
+
module: 'auth',
|
|
27
|
+
dependsOn: ['auth.roles.list'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'auth.acl.manage',
|
|
31
|
+
title: 'Manage ACLs',
|
|
32
|
+
module: 'auth',
|
|
33
|
+
dependsOn: ['auth.users.list', 'auth.roles.list'],
|
|
34
|
+
},
|
|
10
35
|
{ id: 'auth.sidebar.manage', title: 'Manage sidebar presets', module: 'auth' },
|
|
11
36
|
]
|
|
12
37
|
|
|
13
38
|
export default features
|
|
14
|
-
|
|
@@ -200,9 +200,9 @@ export default function CustomersCompaniesPage() {
|
|
|
200
200
|
const [total, setTotal] = React.useState(0)
|
|
201
201
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
202
202
|
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
203
|
-
const [search, setSearch] = React.useState('')
|
|
204
203
|
const pathname = usePathname()
|
|
205
204
|
const searchParams = useSearchParams()
|
|
205
|
+
const [search, setSearch] = React.useState(() => searchParams?.get('search')?.trim() ?? '')
|
|
206
206
|
// One-shot URL hydration used as the hook's initial value. The hook is the
|
|
207
207
|
// single source of truth from this point on — the page MUST NOT keep a
|
|
208
208
|
// parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
|
|
@@ -208,9 +208,9 @@ export default function CustomersPeoplePage() {
|
|
|
208
208
|
const [total, setTotal] = React.useState(0)
|
|
209
209
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
210
210
|
const [totalIsCapped, setTotalIsCapped] = React.useState(false)
|
|
211
|
-
const [search, setSearch] = React.useState('')
|
|
212
211
|
const pathname = usePathname()
|
|
213
212
|
const searchParams = useSearchParams()
|
|
213
|
+
const [search, setSearch] = React.useState(() => searchParams?.get('search')?.trim() ?? '')
|
|
214
214
|
// One-shot URL hydration used as the hook's initial value. The hook is the
|
|
215
215
|
// single source of truth from this point on — the page MUST NOT keep a
|
|
216
216
|
// parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
|
|
@@ -131,6 +131,11 @@ function resolveInjectedColumnValue(
|
|
|
131
131
|
return current;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const SHIPMENTS_PAGE_SIZE = 100;
|
|
135
|
+
// A single order beyond this many shipment pages is pathological; stopping there
|
|
136
|
+
// keeps the shipped state explicitly unresolved rather than silently partial.
|
|
137
|
+
const SHIPMENTS_MAX_PAGES = 50;
|
|
138
|
+
|
|
134
139
|
type SalesDocumentItemsSectionProps = {
|
|
135
140
|
documentId: string;
|
|
136
141
|
kind: "order" | "quote";
|
|
@@ -168,6 +173,12 @@ export function SalesDocumentItemsSection({
|
|
|
168
173
|
const [shippedTotals, setShippedTotals] = React.useState<Map<string, number>>(
|
|
169
174
|
new Map(),
|
|
170
175
|
);
|
|
176
|
+
// Quotes have no shipments at all, so their shipped state is known up front.
|
|
177
|
+
// For orders it stays unknown until every shipment page has been read — an
|
|
178
|
+
// empty map from a pending or failed load must never read as "nothing shipped".
|
|
179
|
+
const [shippedTotalsResolved, setShippedTotalsResolved] = React.useState(
|
|
180
|
+
kind !== "order",
|
|
181
|
+
);
|
|
171
182
|
|
|
172
183
|
const { widgets: allColumnWidgets } = useInjectionDataWidgets(
|
|
173
184
|
extensionPoints.hosts.orderItemColumns.spotId,
|
|
@@ -379,22 +390,31 @@ export function SalesDocumentItemsSection({
|
|
|
379
390
|
const loadShippedTotals = React.useCallback(async () => {
|
|
380
391
|
if (kind !== "order") {
|
|
381
392
|
setShippedTotals(new Map());
|
|
393
|
+
setShippedTotalsResolved(true);
|
|
382
394
|
return;
|
|
383
395
|
}
|
|
396
|
+
setShippedTotals(new Map());
|
|
397
|
+
setShippedTotalsResolved(false);
|
|
384
398
|
try {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
399
|
+
const totals = new Map<string, number>();
|
|
400
|
+
let page = 1;
|
|
401
|
+
let collected = 0;
|
|
402
|
+
let complete = false;
|
|
403
|
+
while (page <= SHIPMENTS_MAX_PAGES) {
|
|
404
|
+
const params = new URLSearchParams({
|
|
405
|
+
page: String(page),
|
|
406
|
+
pageSize: String(SHIPMENTS_PAGE_SIZE),
|
|
407
|
+
orderId: documentId,
|
|
408
|
+
});
|
|
409
|
+
const response = await apiCall<{
|
|
410
|
+
items?: Array<Record<string, unknown>>;
|
|
411
|
+
total?: unknown;
|
|
412
|
+
}>(`/api/sales/shipments?${params.toString()}`, undefined, {
|
|
413
|
+
fallback: { items: [] },
|
|
414
|
+
});
|
|
415
|
+
if (!response.ok || !Array.isArray(response.result?.items)) return;
|
|
416
|
+
const shipments = response.result.items;
|
|
417
|
+
shipments.forEach((shipment) => {
|
|
398
418
|
const entries = Array.isArray(shipment.items)
|
|
399
419
|
? (shipment.items as Array<Record<string, unknown>>)
|
|
400
420
|
: [];
|
|
@@ -412,13 +432,22 @@ export function SalesDocumentItemsSection({
|
|
|
412
432
|
totals.set(lineId, current + quantity);
|
|
413
433
|
});
|
|
414
434
|
});
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
435
|
+
collected += shipments.length;
|
|
436
|
+
const reportedTotal = normalizeNumber(response.result?.total, Number.NaN);
|
|
437
|
+
const hasMore =
|
|
438
|
+
shipments.length >= SHIPMENTS_PAGE_SIZE &&
|
|
439
|
+
(!Number.isFinite(reportedTotal) || collected < reportedTotal);
|
|
440
|
+
if (!hasMore) {
|
|
441
|
+
complete = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
page += 1;
|
|
418
445
|
}
|
|
446
|
+
if (!complete) return;
|
|
447
|
+
setShippedTotals(totals);
|
|
448
|
+
setShippedTotalsResolved(true);
|
|
419
449
|
} catch (err) {
|
|
420
450
|
logger.error('sales.document.shipments.load', { err });
|
|
421
|
-
setShippedTotals(new Map());
|
|
422
451
|
}
|
|
423
452
|
}, [documentId, kind]);
|
|
424
453
|
|
|
@@ -439,6 +468,7 @@ export function SalesDocumentItemsSection({
|
|
|
439
468
|
if (kind !== "order") {
|
|
440
469
|
shipmentsLoadedForDocument.current = null;
|
|
441
470
|
setShippedTotals(new Map());
|
|
471
|
+
setShippedTotalsResolved(true);
|
|
442
472
|
return;
|
|
443
473
|
}
|
|
444
474
|
const key = `${kind}:${documentId}`;
|
|
@@ -964,6 +994,7 @@ export function SalesDocumentItemsSection({
|
|
|
964
994
|
? Math.max(0, shippedTotals.get(lineForEdit.id) ?? 0)
|
|
965
995
|
: 0
|
|
966
996
|
}
|
|
997
|
+
shippedQuantityResolved={shippedTotalsResolved}
|
|
967
998
|
onSaved={async () => {
|
|
968
999
|
await loadItems();
|
|
969
1000
|
emitSalesDocumentTotalsRefresh({ documentId, kind });
|
|
@@ -50,6 +50,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
50
50
|
import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
51
51
|
import { formatMoney, normalizeNumber } from "./lineItemUtils";
|
|
52
52
|
import type { SalesLineRecord } from "./lineItemTypes";
|
|
53
|
+
import { prepareShippedLineUpdatePayload } from "./lineItemShipmentLock";
|
|
53
54
|
import {
|
|
54
55
|
normalizeCustomFieldSubmitValue,
|
|
55
56
|
extractCustomFieldValues,
|
|
@@ -279,6 +280,13 @@ type SalesLineDialogProps = {
|
|
|
279
280
|
tenantId: string | null;
|
|
280
281
|
initialLine?: SalesLineRecord | null;
|
|
281
282
|
shippedQuantity?: number;
|
|
283
|
+
/**
|
|
284
|
+
* Whether `shippedQuantity` reflects a fully resolved shipment state. Defaults
|
|
285
|
+
* to `true` so callers that genuinely know the value keep working; pass `false`
|
|
286
|
+
* while the host is still loading shipments or after the load failed, and the
|
|
287
|
+
* dialog locks pricing instead of assuming the line is unshipped.
|
|
288
|
+
*/
|
|
289
|
+
shippedQuantityResolved?: boolean;
|
|
282
290
|
onOpenChange: (open: boolean) => void;
|
|
283
291
|
onSaved?: () => Promise<void> | void;
|
|
284
292
|
onDraftSaved?: (payload: Record<string, unknown>, lineId: string | null) => Promise<void> | void;
|
|
@@ -475,6 +483,7 @@ export function LineItemDialog({
|
|
|
475
483
|
tenantId,
|
|
476
484
|
initialLine,
|
|
477
485
|
shippedQuantity = 0,
|
|
486
|
+
shippedQuantityResolved = true,
|
|
478
487
|
onOpenChange,
|
|
479
488
|
onSaved,
|
|
480
489
|
onDraftSaved,
|
|
@@ -513,6 +522,23 @@ export function LineItemDialog({
|
|
|
513
522
|
() => (kind === "order" ? "sales/order-lines" : "sales/quote-lines"),
|
|
514
523
|
[kind],
|
|
515
524
|
);
|
|
525
|
+
// A line the caller has not resolved shipment state for is treated as shipped:
|
|
526
|
+
// the server rejects pricing changes on shipped lines, so guessing "unshipped"
|
|
527
|
+
// from a pending or failed shipments load hands the user an edit that cannot
|
|
528
|
+
// be saved. A brand-new line has no shipments by construction.
|
|
529
|
+
const hasExistingLine = Boolean(initialLine);
|
|
530
|
+
const shipmentStateUnknown = kind === "order" && hasExistingLine && !shippedQuantityResolved;
|
|
531
|
+
const isShippedOrderLine =
|
|
532
|
+
kind === "order" &&
|
|
533
|
+
hasExistingLine &&
|
|
534
|
+
(shipmentStateUnknown || shippedQuantity > 0);
|
|
535
|
+
// While the shipments read is unresolved the shipped quantity is unknown, so the
|
|
536
|
+
// only safe floor for a quantity edit is the quantity already stored on the line:
|
|
537
|
+
// whatever turns out to be shipped can never exceed it. Raising stays allowed,
|
|
538
|
+
// exactly as it is once the state resolves.
|
|
539
|
+
const storedQuantity = Number(initialLine?.quantity ?? 0);
|
|
540
|
+
const safeStoredQuantity = Number.isFinite(storedQuantity) ? storedQuantity : 0;
|
|
541
|
+
const quantityFloor = shipmentStateUnknown ? safeStoredQuantity : shippedQuantity;
|
|
516
542
|
const documentKey = kind === "order" ? "orderId" : "quoteId";
|
|
517
543
|
const customFieldEntityId =
|
|
518
544
|
kind === "order" ? E.sales.sales_order_line : E.sales.sales_quote_line;
|
|
@@ -1330,12 +1356,17 @@ export function LineItemDialog({
|
|
|
1330
1356
|
},
|
|
1331
1357
|
);
|
|
1332
1358
|
}
|
|
1333
|
-
if (
|
|
1334
|
-
const message =
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1359
|
+
if (quantityFloor > 0 && qtyNumber < quantityFloor) {
|
|
1360
|
+
const message = shipmentStateUnknown
|
|
1361
|
+
? t(
|
|
1362
|
+
"sales.documents.items.errorQuantityShipmentsUnknown",
|
|
1363
|
+
"The quantity cannot be lowered until this order's shipments have been read. Reopen the order to try again.",
|
|
1364
|
+
)
|
|
1365
|
+
: t(
|
|
1366
|
+
"sales.documents.items.errorQuantityBelowShipped",
|
|
1367
|
+
"You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
1368
|
+
{ shipped: shippedQuantity },
|
|
1369
|
+
);
|
|
1339
1370
|
throw createCrudFormError(message, { quantity: message });
|
|
1340
1371
|
}
|
|
1341
1372
|
const resolvedQuantityUnit = (() => {
|
|
@@ -1509,9 +1540,20 @@ export function LineItemDialog({
|
|
|
1509
1540
|
}
|
|
1510
1541
|
if (resolvedName) payload.name = resolvedName;
|
|
1511
1542
|
|
|
1543
|
+
const submittedPayload = prepareShippedLineUpdatePayload(
|
|
1544
|
+
payload,
|
|
1545
|
+
isShippedOrderLine && initialLine
|
|
1546
|
+
? {
|
|
1547
|
+
quantity: initialLine.quantity,
|
|
1548
|
+
totalNetAmount: initialLine.totalNet,
|
|
1549
|
+
totalGrossAmount: initialLine.totalGross,
|
|
1550
|
+
}
|
|
1551
|
+
: null,
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1512
1554
|
try {
|
|
1513
1555
|
if (onDraftSaved) {
|
|
1514
|
-
await onDraftSaved(
|
|
1556
|
+
await onDraftSaved(submittedPayload, editingId);
|
|
1515
1557
|
closeDialog();
|
|
1516
1558
|
return;
|
|
1517
1559
|
}
|
|
@@ -1521,7 +1563,9 @@ export function LineItemDialog({
|
|
|
1521
1563
|
() =>
|
|
1522
1564
|
action(
|
|
1523
1565
|
resourcePath,
|
|
1524
|
-
editingId
|
|
1566
|
+
editingId
|
|
1567
|
+
? { id: editingId, ...submittedPayload }
|
|
1568
|
+
: submittedPayload,
|
|
1525
1569
|
{
|
|
1526
1570
|
errorMessage: t(
|
|
1527
1571
|
"sales.documents.items.errorSave",
|
|
@@ -1552,10 +1596,15 @@ export function LineItemDialog({
|
|
|
1552
1596
|
documentKey,
|
|
1553
1597
|
documentUpdatedAt,
|
|
1554
1598
|
editingId,
|
|
1599
|
+
initialLine,
|
|
1600
|
+
isShippedOrderLine,
|
|
1555
1601
|
onDraftSaved,
|
|
1556
1602
|
priceOptions,
|
|
1557
1603
|
productOption,
|
|
1604
|
+
quantityFloor,
|
|
1558
1605
|
resourcePath,
|
|
1606
|
+
shipmentStateUnknown,
|
|
1607
|
+
shippedQuantity,
|
|
1559
1608
|
t,
|
|
1560
1609
|
variantOption,
|
|
1561
1610
|
onSaved,
|
|
@@ -1613,6 +1662,7 @@ export function LineItemDialog({
|
|
|
1613
1662
|
type="button"
|
|
1614
1663
|
size="sm"
|
|
1615
1664
|
variant={mode === "catalog" ? "default" : "ghost"}
|
|
1665
|
+
disabled={isShippedOrderLine}
|
|
1616
1666
|
onClick={() => switchMode("catalog")}
|
|
1617
1667
|
>
|
|
1618
1668
|
{t("sales.documents.items.lineMode.catalog", "Catalog item")}
|
|
@@ -1621,6 +1671,7 @@ export function LineItemDialog({
|
|
|
1621
1671
|
type="button"
|
|
1622
1672
|
size="sm"
|
|
1623
1673
|
variant={mode === "custom" ? "default" : "ghost"}
|
|
1674
|
+
disabled={isShippedOrderLine}
|
|
1624
1675
|
onClick={() => switchMode("custom")}
|
|
1625
1676
|
>
|
|
1626
1677
|
{t("sales.documents.items.lineMode.custom", "Custom line")}
|
|
@@ -1801,6 +1852,7 @@ export function LineItemDialog({
|
|
|
1801
1852
|
},
|
|
1802
1853
|
)
|
|
1803
1854
|
}
|
|
1855
|
+
disabled={isShippedOrderLine}
|
|
1804
1856
|
/>
|
|
1805
1857
|
),
|
|
1806
1858
|
} satisfies CrudField,
|
|
@@ -1960,7 +2012,7 @@ export function LineItemDialog({
|
|
|
1960
2012
|
},
|
|
1961
2013
|
)
|
|
1962
2014
|
}
|
|
1963
|
-
disabled={!productId}
|
|
2015
|
+
disabled={isShippedOrderLine || !productId}
|
|
1964
2016
|
/>
|
|
1965
2017
|
);
|
|
1966
2018
|
},
|
|
@@ -1984,6 +2036,51 @@ export function LineItemDialog({
|
|
|
1984
2036
|
typeof values?.variantId === "string"
|
|
1985
2037
|
? values.variantId
|
|
1986
2038
|
: null;
|
|
2039
|
+
const selectedPriceId =
|
|
2040
|
+
typeof value === "string" ? value : null;
|
|
2041
|
+
const selectedPrice = selectedPriceId
|
|
2042
|
+
? (priceOptions.find(
|
|
2043
|
+
(entry) => entry.id === selectedPriceId,
|
|
2044
|
+
) ?? null)
|
|
2045
|
+
: null;
|
|
2046
|
+
if (isShippedOrderLine) {
|
|
2047
|
+
const lockedAmount = normalizeNumber(
|
|
2048
|
+
values?.unitPrice,
|
|
2049
|
+
Number.NaN,
|
|
2050
|
+
);
|
|
2051
|
+
const lockedCurrency =
|
|
2052
|
+
selectedPrice?.currencyCode ??
|
|
2053
|
+
(typeof values?.currencyCode === "string"
|
|
2054
|
+
? values.currencyCode
|
|
2055
|
+
: currencyCode) ??
|
|
2056
|
+
undefined;
|
|
2057
|
+
const lockedModeLabel =
|
|
2058
|
+
values?.priceMode === "net"
|
|
2059
|
+
? t("sales.documents.items.priceNet", "Net")
|
|
2060
|
+
: t("sales.documents.items.priceGross", "Gross");
|
|
2061
|
+
const lockedAmountLabel = Number.isFinite(lockedAmount)
|
|
2062
|
+
? `${formatMoney(lockedAmount, lockedCurrency)} — ${lockedModeLabel}`
|
|
2063
|
+
: lockedModeLabel;
|
|
2064
|
+
const lockedPriceDetail =
|
|
2065
|
+
selectedPrice?.priceKindTitle ??
|
|
2066
|
+
selectedPrice?.priceKindCode ??
|
|
2067
|
+
null;
|
|
2068
|
+
return (
|
|
2069
|
+
<div className="space-y-2">
|
|
2070
|
+
<Input
|
|
2071
|
+
readOnly
|
|
2072
|
+
disabled
|
|
2073
|
+
value={lockedAmountLabel}
|
|
2074
|
+
aria-label={t("sales.documents.items.price", "Price")}
|
|
2075
|
+
/>
|
|
2076
|
+
{lockedPriceDetail ? (
|
|
2077
|
+
<p className="text-xs text-muted-foreground">
|
|
2078
|
+
{lockedPriceDetail}
|
|
2079
|
+
</p>
|
|
2080
|
+
) : null}
|
|
2081
|
+
</div>
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
1987
2084
|
return (
|
|
1988
2085
|
<LookupSelect
|
|
1989
2086
|
key={
|
|
@@ -1991,7 +2088,7 @@ export function LineItemDialog({
|
|
|
1991
2088
|
? `${productId}-${variantId ?? "no-variant"}`
|
|
1992
2089
|
: "price"
|
|
1993
2090
|
}
|
|
1994
|
-
value={
|
|
2091
|
+
value={selectedPriceId}
|
|
1995
2092
|
onChange={(next) => {
|
|
1996
2093
|
setValue(next ?? null);
|
|
1997
2094
|
const selected = next
|
|
@@ -2133,9 +2230,11 @@ export function LineItemDialog({
|
|
|
2133
2230
|
}
|
|
2134
2231
|
onChange={(event) => setValue(event.target.value)}
|
|
2135
2232
|
placeholder="0.00"
|
|
2233
|
+
disabled={isShippedOrderLine}
|
|
2136
2234
|
/>
|
|
2137
2235
|
<Select
|
|
2138
2236
|
value={mode}
|
|
2237
|
+
disabled={isShippedOrderLine}
|
|
2139
2238
|
onValueChange={(value) => {
|
|
2140
2239
|
const nextMode = value === "net" ? "net" : "gross";
|
|
2141
2240
|
setFormValue?.("priceMode", nextMode);
|
|
@@ -2222,7 +2321,7 @@ export function LineItemDialog({
|
|
|
2222
2321
|
<Select
|
|
2223
2322
|
value={resolvedValue || undefined}
|
|
2224
2323
|
onValueChange={(value) => handleChange({ target: { value } } as React.ChangeEvent<HTMLSelectElement>)}
|
|
2225
|
-
disabled={!taxRates.length}
|
|
2324
|
+
disabled={isShippedOrderLine || !taxRates.length}
|
|
2226
2325
|
>
|
|
2227
2326
|
<SelectTrigger>
|
|
2228
2327
|
<SelectValue
|
|
@@ -2292,6 +2391,7 @@ export function LineItemDialog({
|
|
|
2292
2391
|
<Input
|
|
2293
2392
|
value={typeof value === "string" ? value : ""}
|
|
2294
2393
|
onChange={(event) => setValue(event.target.value || null)}
|
|
2394
|
+
disabled={isShippedOrderLine}
|
|
2295
2395
|
placeholder={t(
|
|
2296
2396
|
"sales.documents.items.quantityUnitPlaceholder",
|
|
2297
2397
|
"e.g. pc",
|
|
@@ -2349,7 +2449,7 @@ export function LineItemDialog({
|
|
|
2349
2449
|
});
|
|
2350
2450
|
}
|
|
2351
2451
|
}}
|
|
2352
|
-
disabled={!productId}
|
|
2452
|
+
disabled={isShippedOrderLine || !productId}
|
|
2353
2453
|
>
|
|
2354
2454
|
<SelectTrigger>
|
|
2355
2455
|
<SelectValue
|
|
@@ -2394,7 +2494,7 @@ export function LineItemDialog({
|
|
|
2394
2494
|
typeof values?.quantityUnit === "string"
|
|
2395
2495
|
? values.quantityUnit
|
|
2396
2496
|
: null;
|
|
2397
|
-
if (productId) {
|
|
2497
|
+
if (productId && !isShippedOrderLine) {
|
|
2398
2498
|
const selectedPriceId =
|
|
2399
2499
|
typeof values?.priceId === "string" ? values.priceId : null;
|
|
2400
2500
|
const selectedPriceKindId =
|
|
@@ -2549,6 +2649,7 @@ export function LineItemDialog({
|
|
|
2549
2649
|
resolveTaxSelection,
|
|
2550
2650
|
selectPriceAfterRefresh,
|
|
2551
2651
|
hasTaxMetadata,
|
|
2652
|
+
isShippedOrderLine,
|
|
2552
2653
|
]);
|
|
2553
2654
|
|
|
2554
2655
|
const groups = React.useMemo<CrudFormGroup[]>(() => {
|
|
@@ -2914,6 +3015,21 @@ export function LineItemDialog({
|
|
|
2914
3015
|
: t("sales.documents.items.addTitle", "Add line")}
|
|
2915
3016
|
</DialogTitle>
|
|
2916
3017
|
</DialogHeader>
|
|
3018
|
+
{isShippedOrderLine ? (
|
|
3019
|
+
<Alert status="information" style="lighter">
|
|
3020
|
+
<AlertDescription>
|
|
3021
|
+
{shipmentStateUnknown
|
|
3022
|
+
? t(
|
|
3023
|
+
"sales.documents.items.shippedLineLockPending",
|
|
3024
|
+
"Pricing is locked until this order's shipments have been read. Reopen the order to try again — you can still edit the name and raise the quantity.",
|
|
3025
|
+
)
|
|
3026
|
+
: t(
|
|
3027
|
+
"sales.documents.items.shippedLineLocked",
|
|
3028
|
+
"Pricing is locked on this line because it already has shipped items. You can still edit the name and quantity.",
|
|
3029
|
+
)}
|
|
3030
|
+
</AlertDescription>
|
|
3031
|
+
</Alert>
|
|
3032
|
+
) : null}
|
|
2917
3033
|
<CrudForm<LineFormState>
|
|
2918
3034
|
key={formResetKey}
|
|
2919
3035
|
embedded
|
|
@@ -1311,6 +1311,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
|
|
|
1311
1311
|
id: 'lines',
|
|
1312
1312
|
label: '',
|
|
1313
1313
|
type: 'custom',
|
|
1314
|
+
rendersOwnError: true,
|
|
1314
1315
|
component: ({ value, error, values, setValue }) => values?.documentKind === 'order' ? (
|
|
1315
1316
|
<SalesOrderDraftLines
|
|
1316
1317
|
currencyCode={typeof values.currencyCode === 'string' ? values.currencyCode : defaultCurrency}
|
|
@@ -161,7 +161,7 @@ export function SalesOrderDraftLines({
|
|
|
161
161
|
{t('sales.documents.items.add', 'Add item')}
|
|
162
162
|
</Button>
|
|
163
163
|
</div>
|
|
164
|
-
{error ? <p className="text-sm text-
|
|
164
|
+
{error ? <p className="text-sm text-status-error-text" role="alert">{error}</p> : null}
|
|
165
165
|
<DataTable
|
|
166
166
|
columns={columns}
|
|
167
167
|
data={lines}
|