@open-mercato/core 0.6.6-develop.6525.1.e9a4b56963 → 0.6.6-develop.6529.1.e1de4b3f27
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/customer_accounts/widgets/injection-table.js +0 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +11 -7
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
- package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +44 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +2 -27
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +1 -0
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +9 -0
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/lib/shipments/snapshots.js +27 -1
- package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/AGENTS.md +8 -6
- package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +13 -7
- package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
- package/src/modules/sales/commands/documents.ts +65 -0
- package/src/modules/sales/commands/returns.ts +2 -32
- package/src/modules/sales/components/documents/ItemsSection.tsx +5 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +10 -0
- package/src/modules/sales/i18n/de.json +4 -0
- package/src/modules/sales/i18n/en.json +4 -0
- package/src/modules/sales/i18n/es.json +4 -0
- package/src/modules/sales/i18n/pl.json +4 -0
- package/src/modules/sales/lib/shipments/snapshots.ts +30 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { SalesOrderLine, SalesShipmentItem } from "../../data/entities.js";
|
|
2
|
+
import { SalesOrderLine, SalesShipment, SalesShipmentItem } from "../../data/entities.js";
|
|
3
3
|
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
4
|
const cloneJson = (value) => {
|
|
5
5
|
if (value === null || value === void 0) return value;
|
|
@@ -84,9 +84,35 @@ const refreshShipmentItemsSnapshot = async (em, shipment, options) => {
|
|
|
84
84
|
shipment.itemsSnapshot = snapshot.length ? snapshot : null;
|
|
85
85
|
return snapshot;
|
|
86
86
|
};
|
|
87
|
+
const loadShippedQuantityByLine = async (em, orderId, scope) => {
|
|
88
|
+
const shipments = await findWithDecryption(
|
|
89
|
+
em,
|
|
90
|
+
SalesShipment,
|
|
91
|
+
{ order: orderId, deletedAt: null },
|
|
92
|
+
{},
|
|
93
|
+
scope
|
|
94
|
+
);
|
|
95
|
+
const shippedByLine = /* @__PURE__ */ new Map();
|
|
96
|
+
if (!shipments.length) return shippedByLine;
|
|
97
|
+
const items = await findWithDecryption(
|
|
98
|
+
em,
|
|
99
|
+
SalesShipmentItem,
|
|
100
|
+
{ shipment: { $in: shipments.map((shipment) => shipment.id) } },
|
|
101
|
+
{},
|
|
102
|
+
scope
|
|
103
|
+
);
|
|
104
|
+
items.forEach((item) => {
|
|
105
|
+
const orderLineId = typeof item.orderLine === "string" ? item.orderLine : item.orderLine?.id ?? null;
|
|
106
|
+
if (!orderLineId) return;
|
|
107
|
+
const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity);
|
|
108
|
+
shippedByLine.set(orderLineId, next);
|
|
109
|
+
});
|
|
110
|
+
return shippedByLine;
|
|
111
|
+
};
|
|
87
112
|
export {
|
|
88
113
|
buildShipmentItemSnapshots,
|
|
89
114
|
coerceShipmentQuantity,
|
|
115
|
+
loadShippedQuantityByLine,
|
|
90
116
|
readShipmentItemsSnapshot,
|
|
91
117
|
refreshShipmentItemsSnapshot
|
|
92
118
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sales/lib/shipments/snapshots.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesOrderLine, SalesShipment, SalesShipmentItem } from '../../data/entities'\nimport type { ShipmentItemSnapshot } from './types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst cloneJson = <T>(value: T): T => {\n if (value === null || value === undefined) return value\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport const coerceShipmentQuantity = (value: unknown): number => {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n if (!Number.isNaN(parsed)) return parsed\n }\n return 0\n}\n\nconst extractOrderLineId = (entry: SalesShipmentItem): string | null => {\n const raw =\n typeof entry.orderLine === 'string'\n ? entry.orderLine\n : entry.orderLine?.id ?? (entry as any).orderLineId ?? null\n return typeof raw === 'string' ? raw : null\n}\n\nconst ensureLineMap = async ( // NOSONAR \u2014 returns same Map ref but populated from DB between early return and final return\n em: EntityManager,\n items: SalesShipmentItem[],\n lineMap?: Map<string, SalesOrderLine>\n): Promise<Map<string, SalesOrderLine>> => {\n const map = lineMap ?? new Map<string, SalesOrderLine>()\n const missing: string[] = []\n items.forEach((item) => {\n const lineId = extractOrderLineId(item)\n if (!lineId || map.has(lineId)) return\n missing.push(lineId)\n })\n if (!missing.length) return map\n const lines = await em.find(SalesOrderLine, { id: { $in: missing } })\n lines.forEach((line) => map.set(line.id, line))\n return map\n}\n\nexport const buildShipmentItemSnapshots = (\n items: SalesShipmentItem[],\n options?: { lineMap?: Map<string, SalesOrderLine> }\n): ShipmentItemSnapshot[] => {\n const map = options?.lineMap ?? new Map<string, SalesOrderLine>()\n return items\n .map((item) => {\n const orderLineId = extractOrderLineId(item)\n if (!orderLineId) return null\n const line = map.get(orderLineId) ?? null\n const id = typeof item.id === 'string' ? item.id : randomUUID()\n const quantity = coerceShipmentQuantity(item.quantity)\n const metadata =\n item.metadata && typeof item.metadata === 'object' ? cloneJson(item.metadata) : null\n return {\n id,\n orderLineId,\n orderLineName: line?.name ?? null,\n orderLineNumber: line?.lineNumber ?? null,\n quantity,\n metadata,\n } satisfies ShipmentItemSnapshot\n })\n .filter((entry): entry is ShipmentItemSnapshot => Boolean(entry))\n}\n\nexport const readShipmentItemsSnapshot = (raw: unknown): ShipmentItemSnapshot[] => {\n if (!Array.isArray(raw)) return []\n return raw\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const orderLineId =\n typeof (entry as any).orderLineId === 'string'\n ? (entry as any).orderLineId\n : typeof (entry as any).order_line_id === 'string'\n ? (entry as any).order_line_id\n : null\n if (!orderLineId) return null\n const quantity = coerceShipmentQuantity((entry as any).quantity)\n const orderLineNumberRaw = (entry as any).orderLineNumber ?? (entry as any).order_line_number\n const orderLineNumber =\n typeof orderLineNumberRaw === 'number' && Number.isFinite(orderLineNumberRaw)\n ? orderLineNumberRaw\n : typeof orderLineNumberRaw === 'string' && orderLineNumberRaw.trim().length\n ? Number(orderLineNumberRaw)\n : null\n const metadata =\n (entry as any).metadata && typeof (entry as any).metadata === 'object'\n ? cloneJson((entry as any).metadata as Record<string, unknown>)\n : null\n const orderLineName =\n typeof (entry as any).orderLineName === 'string'\n ? (entry as any).orderLineName\n : typeof (entry as any).order_line_name === 'string'\n ? (entry as any).order_line_name\n : null\n const id =\n typeof (entry as any).id === 'string' && (entry as any).id.length\n ? (entry as any).id\n : randomUUID()\n return {\n id,\n orderLineId,\n orderLineName,\n orderLineNumber: Number.isFinite(orderLineNumber) ? Number(orderLineNumber) : null,\n quantity,\n metadata,\n } satisfies ShipmentItemSnapshot\n })\n .filter((entry): entry is ShipmentItemSnapshot => Boolean(entry))\n}\n\nexport const refreshShipmentItemsSnapshot = async (\n em: EntityManager,\n shipment: SalesShipment,\n options?: { items?: SalesShipmentItem[]; lineMap?: Map<string, SalesOrderLine> }\n): Promise<ShipmentItemSnapshot[]> => {\n const items =\n options?.items ??\n (await findWithDecryption(\n em,\n SalesShipmentItem,\n { shipment },\n { populate: ['orderLine'] },\n { tenantId: shipment.tenantId ?? null, organizationId: shipment.organizationId ?? null },\n ))\n const map = await ensureLineMap(em, items, options?.lineMap)\n const snapshot = buildShipmentItemSnapshots(items, { lineMap: map })\n shipment.itemsSnapshot = snapshot.length ? snapshot : null\n return snapshot\n}\n\nexport type { ShipmentItemSnapshot } from './types'\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesOrderLine, SalesShipment, SalesShipmentItem } from '../../data/entities'\nimport type { ShipmentItemSnapshot } from './types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst cloneJson = <T>(value: T): T => {\n if (value === null || value === undefined) return value\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nexport const coerceShipmentQuantity = (value: unknown): number => {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n if (!Number.isNaN(parsed)) return parsed\n }\n return 0\n}\n\nconst extractOrderLineId = (entry: SalesShipmentItem): string | null => {\n const raw =\n typeof entry.orderLine === 'string'\n ? entry.orderLine\n : entry.orderLine?.id ?? (entry as any).orderLineId ?? null\n return typeof raw === 'string' ? raw : null\n}\n\nconst ensureLineMap = async ( // NOSONAR \u2014 returns same Map ref but populated from DB between early return and final return\n em: EntityManager,\n items: SalesShipmentItem[],\n lineMap?: Map<string, SalesOrderLine>\n): Promise<Map<string, SalesOrderLine>> => {\n const map = lineMap ?? new Map<string, SalesOrderLine>()\n const missing: string[] = []\n items.forEach((item) => {\n const lineId = extractOrderLineId(item)\n if (!lineId || map.has(lineId)) return\n missing.push(lineId)\n })\n if (!missing.length) return map\n const lines = await em.find(SalesOrderLine, { id: { $in: missing } })\n lines.forEach((line) => map.set(line.id, line))\n return map\n}\n\nexport const buildShipmentItemSnapshots = (\n items: SalesShipmentItem[],\n options?: { lineMap?: Map<string, SalesOrderLine> }\n): ShipmentItemSnapshot[] => {\n const map = options?.lineMap ?? new Map<string, SalesOrderLine>()\n return items\n .map((item) => {\n const orderLineId = extractOrderLineId(item)\n if (!orderLineId) return null\n const line = map.get(orderLineId) ?? null\n const id = typeof item.id === 'string' ? item.id : randomUUID()\n const quantity = coerceShipmentQuantity(item.quantity)\n const metadata =\n item.metadata && typeof item.metadata === 'object' ? cloneJson(item.metadata) : null\n return {\n id,\n orderLineId,\n orderLineName: line?.name ?? null,\n orderLineNumber: line?.lineNumber ?? null,\n quantity,\n metadata,\n } satisfies ShipmentItemSnapshot\n })\n .filter((entry): entry is ShipmentItemSnapshot => Boolean(entry))\n}\n\nexport const readShipmentItemsSnapshot = (raw: unknown): ShipmentItemSnapshot[] => {\n if (!Array.isArray(raw)) return []\n return raw\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const orderLineId =\n typeof (entry as any).orderLineId === 'string'\n ? (entry as any).orderLineId\n : typeof (entry as any).order_line_id === 'string'\n ? (entry as any).order_line_id\n : null\n if (!orderLineId) return null\n const quantity = coerceShipmentQuantity((entry as any).quantity)\n const orderLineNumberRaw = (entry as any).orderLineNumber ?? (entry as any).order_line_number\n const orderLineNumber =\n typeof orderLineNumberRaw === 'number' && Number.isFinite(orderLineNumberRaw)\n ? orderLineNumberRaw\n : typeof orderLineNumberRaw === 'string' && orderLineNumberRaw.trim().length\n ? Number(orderLineNumberRaw)\n : null\n const metadata =\n (entry as any).metadata && typeof (entry as any).metadata === 'object'\n ? cloneJson((entry as any).metadata as Record<string, unknown>)\n : null\n const orderLineName =\n typeof (entry as any).orderLineName === 'string'\n ? (entry as any).orderLineName\n : typeof (entry as any).order_line_name === 'string'\n ? (entry as any).order_line_name\n : null\n const id =\n typeof (entry as any).id === 'string' && (entry as any).id.length\n ? (entry as any).id\n : randomUUID()\n return {\n id,\n orderLineId,\n orderLineName,\n orderLineNumber: Number.isFinite(orderLineNumber) ? Number(orderLineNumber) : null,\n quantity,\n metadata,\n } satisfies ShipmentItemSnapshot\n })\n .filter((entry): entry is ShipmentItemSnapshot => Boolean(entry))\n}\n\nexport const refreshShipmentItemsSnapshot = async (\n em: EntityManager,\n shipment: SalesShipment,\n options?: { items?: SalesShipmentItem[]; lineMap?: Map<string, SalesOrderLine> }\n): Promise<ShipmentItemSnapshot[]> => {\n const items =\n options?.items ??\n (await findWithDecryption(\n em,\n SalesShipmentItem,\n { shipment },\n { populate: ['orderLine'] },\n { tenantId: shipment.tenantId ?? null, organizationId: shipment.organizationId ?? null },\n ))\n const map = await ensureLineMap(em, items, options?.lineMap)\n const snapshot = buildShipmentItemSnapshots(items, { lineMap: map })\n shipment.itemsSnapshot = snapshot.length ? snapshot : null\n return snapshot\n}\n\nexport const loadShippedQuantityByLine = async (\n em: EntityManager,\n orderId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<Map<string, number>> => {\n const shipments = await findWithDecryption(\n em,\n SalesShipment,\n { order: orderId, deletedAt: null },\n {},\n scope,\n )\n const shippedByLine = new Map<string, number>()\n if (!shipments.length) return shippedByLine\n const items = await findWithDecryption(\n em,\n SalesShipmentItem,\n { shipment: { $in: shipments.map((shipment) => shipment.id) } },\n {},\n scope,\n )\n items.forEach((item) => {\n const orderLineId = typeof item.orderLine === 'string' ? item.orderLine : (item.orderLine?.id ?? null)\n if (!orderLineId) return\n const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity)\n shippedByLine.set(orderLineId, next)\n })\n return shippedByLine\n}\n\nexport type { ShipmentItemSnapshot } from './types'\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -253,16 +253,16 @@
|
|
|
253
253
|
"zod": "^4.4.3"
|
|
254
254
|
},
|
|
255
255
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
259
259
|
"react": "^19.0.0",
|
|
260
260
|
"react-dom": "^19.0.0"
|
|
261
261
|
},
|
|
262
262
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
263
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
264
|
+
"@open-mercato/shared": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
265
|
+
"@open-mercato/ui": "0.6.6-develop.6529.1.e1de4b3f27",
|
|
266
266
|
"@testing-library/dom": "^10.4.1",
|
|
267
267
|
"@testing-library/jest-dom": "^6.9.1",
|
|
268
268
|
"@testing-library/react": "^16.3.1",
|
|
@@ -306,15 +306,17 @@ Both link to `/backend/customer_accounts/{sourceEntityId}` for staff review.
|
|
|
306
306
|
|
|
307
307
|
| Spot ID | Widget | Purpose |
|
|
308
308
|
|---------|--------|---------|
|
|
309
|
-
| `customers.person` | `account-status` |
|
|
310
|
-
| `crud-form:customers.person` | `account-status` |
|
|
311
|
-
| `
|
|
312
|
-
| `customers.company` | `company-users` | Shows portal users on
|
|
313
|
-
| `crud-form:customers.company` | `company-users` | Alias for CrudForm hosts deriving the spot from `customers.company` |
|
|
314
|
-
| `crud-form:customers:customer_company_profile:fields` | `company-users` | Shows portal users linked to a CRM company |
|
|
309
|
+
| `customers.person` | `account-status` | Alias for hosts requesting the bare `customers.person` entity spot |
|
|
310
|
+
| `crud-form:customers.person` | `account-status` | Shows portal account status on the person v2 detail page (`people-v2/[id]`) |
|
|
311
|
+
| `customers.company` | `company-users` | Alias for hosts requesting the bare `customers.company` entity spot |
|
|
312
|
+
| `crud-form:customers.company` | `company-users` | Shows portal users on the company v2 detail page (`companies-v2/[id]`) |
|
|
315
313
|
|
|
316
314
|
Both inject as column 2 groups with priority 200, gated by `customer_accounts.view` feature.
|
|
317
315
|
|
|
316
|
+
CrudForm derives its spot id as `crud-form:${entityId}` with every `:` normalized to `.`, so a
|
|
317
|
+
registered `crud-form:` key whose entity segment still contains `:` can never be requested by any
|
|
318
|
+
host. Register against the normalized (dot) form only — `injection-table.test.ts` enforces this.
|
|
319
|
+
|
|
318
320
|
## Backend Pages
|
|
319
321
|
|
|
320
322
|
| Path | Purpose |
|
|
@@ -19,10 +19,8 @@ const companyUsersWidget = {
|
|
|
19
19
|
export const injectionTable: ModuleInjectionTable = {
|
|
20
20
|
'customers.person': [accountStatusWidget],
|
|
21
21
|
'crud-form:customers.person': [accountStatusWidget],
|
|
22
|
-
'crud-form:customers:customer_person_profile:fields': [accountStatusWidget],
|
|
23
22
|
'customers.company': [companyUsersWidget],
|
|
24
23
|
'crud-form:customers.company': [companyUsersWidget],
|
|
25
|
-
'crud-form:customers:customer_company_profile:fields': [companyUsersWidget],
|
|
26
24
|
// Step 4.10 — Portal AiChat injection example.
|
|
27
25
|
// Mapped to the portal profile page's `pageAfter('profile')` spot;
|
|
28
26
|
// third-party modules targeting other portal pages can copy this entry.
|
|
@@ -31,6 +31,7 @@ import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
|
31
31
|
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
32
32
|
import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
|
|
33
33
|
import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
34
|
+
import { useDealsAccess } from '../../../../components/detail/useDealsAccess'
|
|
34
35
|
import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
|
|
35
36
|
import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
|
|
36
37
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
@@ -69,6 +70,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
69
70
|
}, [searchParams])
|
|
70
71
|
const [activeTab, setActiveTab] = React.useState<CompanyTabId>(initialTab)
|
|
71
72
|
const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
|
|
73
|
+
const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
|
|
72
74
|
|
|
73
75
|
// Form state
|
|
74
76
|
const [isDirty, setIsDirty] = React.useState(false)
|
|
@@ -328,6 +330,14 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
328
330
|
setSectionAction(null)
|
|
329
331
|
}, [activeTab])
|
|
330
332
|
|
|
333
|
+
// A `?tab=deals` deep link must not strand users without `customers.deals.view`
|
|
334
|
+
// on a tab that no longer exists for them. Wait for the granted features to load
|
|
335
|
+
// so a permitted user is never bounced off the tab mid-fetch.
|
|
336
|
+
React.useEffect(() => {
|
|
337
|
+
if (!isDealsAccessReady || canViewDeals) return
|
|
338
|
+
setActiveTab((current) => (current === 'deals' ? 'people' : current))
|
|
339
|
+
}, [isDealsAccessReady, canViewDeals])
|
|
340
|
+
|
|
331
341
|
// Deals scope
|
|
332
342
|
const dealsScope = React.useMemo(
|
|
333
343
|
() => (currentCompanyId ? ({ kind: 'company', entityId: currentCompanyId } as const) : null),
|
|
@@ -544,7 +554,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
544
554
|
/>
|
|
545
555
|
)}
|
|
546
556
|
|
|
547
|
-
{activeTab === 'deals' && (
|
|
557
|
+
{activeTab === 'deals' && canViewDeals && (
|
|
548
558
|
<DealsSection
|
|
549
559
|
scope={dealsScope}
|
|
550
560
|
emptyLabel={t('customers.companies.detail.empty.deals', 'No deals linked to this company.')}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Plus,
|
|
15
15
|
} from 'lucide-react'
|
|
16
16
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
17
|
+
import { useDealsAccess } from './useDealsAccess'
|
|
17
18
|
|
|
18
19
|
export type CompanyTabId =
|
|
19
20
|
| 'people'
|
|
@@ -87,6 +88,7 @@ export function CompanyDetailTabs({
|
|
|
87
88
|
children,
|
|
88
89
|
}: CompanyDetailTabsProps) {
|
|
89
90
|
const t = useT()
|
|
91
|
+
const { canViewDeals } = useDealsAccess()
|
|
90
92
|
|
|
91
93
|
const builtInTabs: TabDef[] = React.useMemo(
|
|
92
94
|
() => [
|
|
@@ -96,12 +98,16 @@ export function CompanyDetailTabs({
|
|
|
96
98
|
icon: <Users className="size-4" />,
|
|
97
99
|
badge: <CountBadge count={peopleCount} />,
|
|
98
100
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
...(canViewDeals
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
id: 'deals' as CompanyTabId,
|
|
105
|
+
label: t('customers.companies.detail.tabs.deals', 'Deals'),
|
|
106
|
+
icon: <Handshake className="size-4" />,
|
|
107
|
+
badge: <CountBadge count={dealsCount} />,
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
105
111
|
{
|
|
106
112
|
id: 'activity-log',
|
|
107
113
|
label: t('customers.companies.detail.tabs.activityLog', 'Activity log'),
|
|
@@ -121,7 +127,7 @@ export function CompanyDetailTabs({
|
|
|
121
127
|
badge: <CountBadge count={filesCount} />,
|
|
122
128
|
},
|
|
123
129
|
],
|
|
124
|
-
[t, peopleCount, dealsCount, activitiesCount, filesCount],
|
|
130
|
+
[t, canViewDeals, peopleCount, dealsCount, activitiesCount, filesCount],
|
|
125
131
|
)
|
|
126
132
|
|
|
127
133
|
const allTabs: TabDef[] = React.useMemo(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
|
|
5
|
+
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
6
|
+
|
|
7
|
+
export type DealsAccess = {
|
|
8
|
+
canViewDeals: boolean
|
|
9
|
+
isReady: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useDealsAccess(): DealsAccess {
|
|
13
|
+
const { payload, isReady } = useBackendChrome()
|
|
14
|
+
const canViewDeals = React.useMemo(
|
|
15
|
+
() => hasFeature(payload?.grantedFeatures ?? [], 'customers.deals.view'),
|
|
16
|
+
[payload?.grantedFeatures],
|
|
17
|
+
)
|
|
18
|
+
return { canViewDeals, isReady }
|
|
19
|
+
}
|
|
@@ -125,6 +125,7 @@ import {
|
|
|
125
125
|
type SalesLineCalculationResult,
|
|
126
126
|
type SalesDocumentCalculationResult,
|
|
127
127
|
} from "../lib/types";
|
|
128
|
+
import { loadShippedQuantityByLine } from "../lib/shipments/snapshots";
|
|
128
129
|
import { resolveDictionaryEntryValue } from "../lib/dictionaries";
|
|
129
130
|
import { resolveStatusEntryIdByValue } from "../lib/statusHelpers";
|
|
130
131
|
import { SalesDocumentNumberGenerator } from "../services/salesDocumentNumberGenerator";
|
|
@@ -6521,6 +6522,69 @@ const quoteAdjustmentDeleteSchema = z.object({
|
|
|
6521
6522
|
quoteId: z.string().uuid(),
|
|
6522
6523
|
});
|
|
6523
6524
|
|
|
6525
|
+
const SHIPPED_QUANTITY_TOLERANCE = 1e-6;
|
|
6526
|
+
|
|
6527
|
+
const hasNumericChange = (
|
|
6528
|
+
next: number | null | undefined,
|
|
6529
|
+
previous: number | null | undefined,
|
|
6530
|
+
): boolean => {
|
|
6531
|
+
if (next === undefined || next === null) return false;
|
|
6532
|
+
if (previous === undefined || previous === null) return true;
|
|
6533
|
+
return Math.abs(next - previous) > SHIPPED_QUANTITY_TOLERANCE;
|
|
6534
|
+
};
|
|
6535
|
+
|
|
6536
|
+
const hasUnitChange = (
|
|
6537
|
+
next: string | null | undefined,
|
|
6538
|
+
previous: string | null | undefined,
|
|
6539
|
+
): boolean => {
|
|
6540
|
+
if (next === undefined || next === null) return false;
|
|
6541
|
+
const normalizedPrevious = (previous ?? "").trim().toLowerCase();
|
|
6542
|
+
if (!normalizedPrevious) return false;
|
|
6543
|
+
return next.trim().toLowerCase() !== normalizedPrevious;
|
|
6544
|
+
};
|
|
6545
|
+
|
|
6546
|
+
async function assertShippedOrderLineEditable(
|
|
6547
|
+
em: EntityManager,
|
|
6548
|
+
order: SalesOrder,
|
|
6549
|
+
existingSnapshot: SalesLineSnapshot | null,
|
|
6550
|
+
parsed: z.infer<typeof orderLineUpsertSchema>,
|
|
6551
|
+
): Promise<void> {
|
|
6552
|
+
const lineId = existingSnapshot?.id;
|
|
6553
|
+
if (!existingSnapshot || !lineId) return;
|
|
6554
|
+
const shippedByLine = await loadShippedQuantityByLine(em, order.id, {
|
|
6555
|
+
tenantId: order.tenantId,
|
|
6556
|
+
organizationId: order.organizationId,
|
|
6557
|
+
});
|
|
6558
|
+
const shippedQuantity = shippedByLine.get(lineId) ?? 0;
|
|
6559
|
+
if (shippedQuantity <= 0) return;
|
|
6560
|
+
|
|
6561
|
+
const { translate } = await resolveTranslations();
|
|
6562
|
+
const nextQuantity = parsed.quantity ?? existingSnapshot.quantity;
|
|
6563
|
+
if (nextQuantity < shippedQuantity - SHIPPED_QUANTITY_TOLERANCE) {
|
|
6564
|
+
throw new CrudHttpError(409, {
|
|
6565
|
+
error: translate(
|
|
6566
|
+
"sales.documents.items.errorQuantityBelowShipped",
|
|
6567
|
+
"You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
6568
|
+
{ shipped: shippedQuantity },
|
|
6569
|
+
),
|
|
6570
|
+
});
|
|
6571
|
+
}
|
|
6572
|
+
|
|
6573
|
+
const pricingChanged =
|
|
6574
|
+
hasNumericChange(parsed.unitPriceNet, existingSnapshot.unitPriceNet) ||
|
|
6575
|
+
hasNumericChange(parsed.unitPriceGross, existingSnapshot.unitPriceGross) ||
|
|
6576
|
+
hasNumericChange(parsed.taxRate, existingSnapshot.taxRate) ||
|
|
6577
|
+
hasUnitChange(parsed.quantityUnit, existingSnapshot.quantityUnit);
|
|
6578
|
+
if (pricingChanged) {
|
|
6579
|
+
throw new CrudHttpError(409, {
|
|
6580
|
+
error: translate(
|
|
6581
|
+
"sales.documents.items.errorPriceShipped",
|
|
6582
|
+
"You cannot change the price or unit of a line that has shipped items.",
|
|
6583
|
+
),
|
|
6584
|
+
});
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6524
6588
|
const orderLineUpsertCommand: CommandHandler<
|
|
6525
6589
|
{ body?: Record<string, unknown>; query?: Record<string, unknown> },
|
|
6526
6590
|
{ orderId: string; lineId: string }
|
|
@@ -6565,6 +6629,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
6565
6629
|
const existingSnapshot = parsed.id
|
|
6566
6630
|
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
6567
6631
|
: null;
|
|
6632
|
+
await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
|
|
6568
6633
|
const priceMode =
|
|
6569
6634
|
parsed.priceMode === "gross"
|
|
6570
6635
|
? "gross"
|
|
@@ -15,8 +15,8 @@ import type { SalesCalculationService } from '../services/salesCalculationServic
|
|
|
15
15
|
import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
|
|
16
16
|
import { cloneJson, deriveLineNetFromGross, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
17
17
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
18
|
-
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine
|
|
19
|
-
import {
|
|
18
|
+
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
19
|
+
import { loadShippedQuantityByLine } from '../lib/shipments/snapshots'
|
|
20
20
|
import { computeAvailableReturnQuantity } from '../lib/returnQuantity'
|
|
21
21
|
import {
|
|
22
22
|
returnCreateSchema,
|
|
@@ -581,36 +581,6 @@ async function restoreReturnEffects(
|
|
|
581
581
|
return createdLines
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
async function loadShippedQuantityByLine(
|
|
585
|
-
em: EntityManager,
|
|
586
|
-
orderId: string,
|
|
587
|
-
scope: { tenantId: string; organizationId: string },
|
|
588
|
-
): Promise<Map<string, number>> {
|
|
589
|
-
const shipments = await findWithDecryption(
|
|
590
|
-
em,
|
|
591
|
-
SalesShipment,
|
|
592
|
-
{ order: orderId, deletedAt: null },
|
|
593
|
-
{},
|
|
594
|
-
scope,
|
|
595
|
-
)
|
|
596
|
-
const shippedByLine = new Map<string, number>()
|
|
597
|
-
if (!shipments.length) return shippedByLine
|
|
598
|
-
const items = await findWithDecryption(
|
|
599
|
-
em,
|
|
600
|
-
SalesShipmentItem,
|
|
601
|
-
{ shipment: { $in: shipments.map((shipment) => shipment.id) } },
|
|
602
|
-
{},
|
|
603
|
-
scope,
|
|
604
|
-
)
|
|
605
|
-
items.forEach((item) => {
|
|
606
|
-
const orderLineId = typeof item.orderLine === 'string' ? item.orderLine : item.orderLine?.id ?? null
|
|
607
|
-
if (!orderLineId) return
|
|
608
|
-
const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity)
|
|
609
|
-
shippedByLine.set(orderLineId, next)
|
|
610
|
-
})
|
|
611
|
-
return shippedByLine
|
|
612
|
-
}
|
|
613
|
-
|
|
614
584
|
function normalizeLinesInput(lines: ReturnCreateInput['lines']): ReturnLineInput[] {
|
|
615
585
|
const seen = new Set<string>()
|
|
616
586
|
const result: ReturnLineInput[] = []
|
|
@@ -817,6 +817,11 @@ export function SalesDocumentItemsSection({
|
|
|
817
817
|
organizationId={resolvedOrganizationId}
|
|
818
818
|
tenantId={resolvedTenantId}
|
|
819
819
|
initialLine={lineForEdit}
|
|
820
|
+
shippedQuantity={
|
|
821
|
+
lineForEdit
|
|
822
|
+
? Math.max(0, shippedTotals.get(lineForEdit.id) ?? 0)
|
|
823
|
+
: 0
|
|
824
|
+
}
|
|
820
825
|
onSaved={async () => {
|
|
821
826
|
await loadItems();
|
|
822
827
|
emitSalesDocumentTotalsRefresh({ documentId, kind });
|
|
@@ -278,6 +278,7 @@ type SalesLineDialogProps = {
|
|
|
278
278
|
organizationId: string | null;
|
|
279
279
|
tenantId: string | null;
|
|
280
280
|
initialLine?: SalesLineRecord | null;
|
|
281
|
+
shippedQuantity?: number;
|
|
281
282
|
onOpenChange: (open: boolean) => void;
|
|
282
283
|
onSaved?: () => Promise<void> | void;
|
|
283
284
|
};
|
|
@@ -472,6 +473,7 @@ export function LineItemDialog({
|
|
|
472
473
|
organizationId,
|
|
473
474
|
tenantId,
|
|
474
475
|
initialLine,
|
|
476
|
+
shippedQuantity = 0,
|
|
475
477
|
onOpenChange,
|
|
476
478
|
onSaved,
|
|
477
479
|
}: SalesLineDialogProps) {
|
|
@@ -1326,6 +1328,14 @@ export function LineItemDialog({
|
|
|
1326
1328
|
},
|
|
1327
1329
|
);
|
|
1328
1330
|
}
|
|
1331
|
+
if (shippedQuantity > 0 && qtyNumber < shippedQuantity) {
|
|
1332
|
+
const message = t(
|
|
1333
|
+
"sales.documents.items.errorQuantityBelowShipped",
|
|
1334
|
+
"You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
1335
|
+
{ shipped: shippedQuantity },
|
|
1336
|
+
);
|
|
1337
|
+
throw createCrudFormError(message, { quantity: message });
|
|
1338
|
+
}
|
|
1329
1339
|
const resolvedQuantityUnit = (() => {
|
|
1330
1340
|
const entered = normalizeUnitCode(values.quantityUnit);
|
|
1331
1341
|
if (isCustomLine) return entered;
|
|
@@ -638,8 +638,10 @@
|
|
|
638
638
|
"sales.documents.detail.items.errorDelete": "Position konnte nicht gelöscht werden.",
|
|
639
639
|
"sales.documents.detail.items.errorDeleteShipped": "Positionen mit versandten Artikeln können nicht gelöscht werden.",
|
|
640
640
|
"sales.documents.detail.items.errorLoad": "Positionen konnten nicht geladen werden.",
|
|
641
|
+
"sales.documents.detail.items.errorPriceShipped": "Preis oder Einheit einer Position mit versandten Artikeln können nicht geändert werden.",
|
|
641
642
|
"sales.documents.detail.items.errorProductRequired": "Wähle ein Produkt aus, um fortzufahren.",
|
|
642
643
|
"sales.documents.detail.items.errorQuantity": "Die Menge muss größer als 0 sein.",
|
|
644
|
+
"sales.documents.detail.items.errorQuantityBelowShipped": "Die Menge kann nicht unter die bereits versandten {{shipped}} gesenkt werden.",
|
|
643
645
|
"sales.documents.detail.items.errorSave": "Position konnte nicht gespeichert werden.",
|
|
644
646
|
"sales.documents.detail.items.errorUnitPrice": "Der Stückpreis muss größer als 0 sein.",
|
|
645
647
|
"sales.documents.detail.items.errorVariantRequired": "Wähle eine Variante aus, um fortzufahren.",
|
|
@@ -950,8 +952,10 @@
|
|
|
950
952
|
"sales.documents.items.errorDeleteShipped": "Positionen mit versandten Artikeln können nicht gelöscht werden.",
|
|
951
953
|
"sales.documents.items.errorLoad": "Positionen konnten nicht geladen werden.",
|
|
952
954
|
"sales.documents.items.errorNameRequired": "Name ist für benutzerdefinierte Positionen erforderlich.",
|
|
955
|
+
"sales.documents.items.errorPriceShipped": "Preis oder Einheit einer Position mit versandten Artikeln können nicht geändert werden.",
|
|
953
956
|
"sales.documents.items.errorProductRequired": "Wähle ein Produkt aus, um fortzufahren.",
|
|
954
957
|
"sales.documents.items.errorQuantity": "Die Menge muss größer als 0 sein.",
|
|
958
|
+
"sales.documents.items.errorQuantityBelowShipped": "Die Menge kann nicht unter die bereits versandten {{shipped}} gesenkt werden.",
|
|
955
959
|
"sales.documents.items.errorSave": "Position konnte nicht gespeichert werden.",
|
|
956
960
|
"sales.documents.items.errorScope": "Organisation und Mandant sind erforderlich.",
|
|
957
961
|
"sales.documents.items.errorUnitPrice": "Der Stückpreis muss größer als 0 sein.",
|
|
@@ -638,8 +638,10 @@
|
|
|
638
638
|
"sales.documents.detail.items.errorDelete": "Failed to delete line.",
|
|
639
639
|
"sales.documents.detail.items.errorDeleteShipped": "You cannot delete a line that has shipped items.",
|
|
640
640
|
"sales.documents.detail.items.errorLoad": "Failed to load items.",
|
|
641
|
+
"sales.documents.detail.items.errorPriceShipped": "You cannot change the price or unit of a line that has shipped items.",
|
|
641
642
|
"sales.documents.detail.items.errorProductRequired": "Select a product to continue.",
|
|
642
643
|
"sales.documents.detail.items.errorQuantity": "Quantity must be greater than 0.",
|
|
644
|
+
"sales.documents.detail.items.errorQuantityBelowShipped": "You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
643
645
|
"sales.documents.detail.items.errorSave": "Failed to save line.",
|
|
644
646
|
"sales.documents.detail.items.errorUnitPrice": "Unit price must be greater than 0.",
|
|
645
647
|
"sales.documents.detail.items.errorVariantRequired": "Select a variant to continue.",
|
|
@@ -950,8 +952,10 @@
|
|
|
950
952
|
"sales.documents.items.errorDeleteShipped": "You cannot delete a line that has shipped items.",
|
|
951
953
|
"sales.documents.items.errorLoad": "Failed to load items.",
|
|
952
954
|
"sales.documents.items.errorNameRequired": "Name is required for custom lines.",
|
|
955
|
+
"sales.documents.items.errorPriceShipped": "You cannot change the price or unit of a line that has shipped items.",
|
|
953
956
|
"sales.documents.items.errorProductRequired": "Select a product to continue.",
|
|
954
957
|
"sales.documents.items.errorQuantity": "Quantity must be greater than 0.",
|
|
958
|
+
"sales.documents.items.errorQuantityBelowShipped": "You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
955
959
|
"sales.documents.items.errorSave": "Failed to save line.",
|
|
956
960
|
"sales.documents.items.errorScope": "Organization and tenant are required.",
|
|
957
961
|
"sales.documents.items.errorUnitPrice": "Unit price must be greater than 0.",
|
|
@@ -638,8 +638,10 @@
|
|
|
638
638
|
"sales.documents.detail.items.errorDelete": "No se pudo eliminar la línea.",
|
|
639
639
|
"sales.documents.detail.items.errorDeleteShipped": "No puedes eliminar una línea que ya tiene envíos.",
|
|
640
640
|
"sales.documents.detail.items.errorLoad": "No se pudieron cargar los artículos.",
|
|
641
|
+
"sales.documents.detail.items.errorPriceShipped": "No puedes cambiar el precio ni la unidad de una línea que ya tiene envíos.",
|
|
641
642
|
"sales.documents.detail.items.errorProductRequired": "Selecciona un producto para continuar.",
|
|
642
643
|
"sales.documents.detail.items.errorQuantity": "La cantidad debe ser mayor que 0.",
|
|
644
|
+
"sales.documents.detail.items.errorQuantityBelowShipped": "No puedes reducir la cantidad por debajo de las {{shipped}} ya enviadas.",
|
|
643
645
|
"sales.documents.detail.items.errorSave": "No se pudo guardar la línea.",
|
|
644
646
|
"sales.documents.detail.items.errorUnitPrice": "El precio unitario debe ser mayor que 0.",
|
|
645
647
|
"sales.documents.detail.items.errorVariantRequired": "Selecciona una variante para continuar.",
|
|
@@ -950,8 +952,10 @@
|
|
|
950
952
|
"sales.documents.items.errorDeleteShipped": "No puedes eliminar una línea que ya tiene envíos.",
|
|
951
953
|
"sales.documents.items.errorLoad": "No se pudieron cargar los artículos.",
|
|
952
954
|
"sales.documents.items.errorNameRequired": "El nombre es obligatorio para líneas personalizadas.",
|
|
955
|
+
"sales.documents.items.errorPriceShipped": "No puedes cambiar el precio ni la unidad de una línea que ya tiene envíos.",
|
|
953
956
|
"sales.documents.items.errorProductRequired": "Selecciona un producto para continuar.",
|
|
954
957
|
"sales.documents.items.errorQuantity": "La cantidad debe ser mayor que 0.",
|
|
958
|
+
"sales.documents.items.errorQuantityBelowShipped": "No puedes reducir la cantidad por debajo de las {{shipped}} ya enviadas.",
|
|
955
959
|
"sales.documents.items.errorSave": "No se pudo guardar la línea.",
|
|
956
960
|
"sales.documents.items.errorScope": "Se requieren organización e inquilino.",
|
|
957
961
|
"sales.documents.items.errorUnitPrice": "El precio unitario debe ser mayor que 0.",
|
|
@@ -638,8 +638,10 @@
|
|
|
638
638
|
"sales.documents.detail.items.errorDelete": "Nie udało się usunąć pozycji.",
|
|
639
639
|
"sales.documents.detail.items.errorDeleteShipped": "Nie można usunąć pozycji, która ma wysłane sztuki.",
|
|
640
640
|
"sales.documents.detail.items.errorLoad": "Nie udało się wczytać pozycji.",
|
|
641
|
+
"sales.documents.detail.items.errorPriceShipped": "Nie można zmienić ceny ani jednostki pozycji, która ma wysłane sztuki.",
|
|
641
642
|
"sales.documents.detail.items.errorProductRequired": "Wybierz produkt, aby kontynuować.",
|
|
642
643
|
"sales.documents.detail.items.errorQuantity": "Ilość musi być większa niż 0.",
|
|
644
|
+
"sales.documents.detail.items.errorQuantityBelowShipped": "Nie można obniżyć ilości poniżej {{shipped}} już wysłanych.",
|
|
643
645
|
"sales.documents.detail.items.errorSave": "Nie udało się zapisać pozycji.",
|
|
644
646
|
"sales.documents.detail.items.errorUnitPrice": "Cena jednostkowa musi być większa niż 0.",
|
|
645
647
|
"sales.documents.detail.items.errorVariantRequired": "Wybierz wariant, aby kontynuować.",
|
|
@@ -950,8 +952,10 @@
|
|
|
950
952
|
"sales.documents.items.errorDeleteShipped": "Nie można usunąć pozycji, która ma wysłane sztuki.",
|
|
951
953
|
"sales.documents.items.errorLoad": "Nie udało się załadować pozycji.",
|
|
952
954
|
"sales.documents.items.errorNameRequired": "Nazwa jest wymagana dla niestandardowych pozycji.",
|
|
955
|
+
"sales.documents.items.errorPriceShipped": "Nie można zmienić ceny ani jednostki pozycji, która ma wysłane sztuki.",
|
|
953
956
|
"sales.documents.items.errorProductRequired": "Wybierz produkt, aby kontynuować.",
|
|
954
957
|
"sales.documents.items.errorQuantity": "Ilość musi być większa niż 0.",
|
|
958
|
+
"sales.documents.items.errorQuantityBelowShipped": "Nie można obniżyć ilości poniżej {{shipped}} już wysłanych.",
|
|
955
959
|
"sales.documents.items.errorSave": "Nie udało się zapisać pozycji.",
|
|
956
960
|
"sales.documents.items.errorScope": "Wymagane są organizacja i najemca.",
|
|
957
961
|
"sales.documents.items.errorUnitPrice": "Cena jednostkowa musi być większa niż 0.",
|
|
@@ -136,4 +136,34 @@ export const refreshShipmentItemsSnapshot = async (
|
|
|
136
136
|
return snapshot
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
export const loadShippedQuantityByLine = async (
|
|
140
|
+
em: EntityManager,
|
|
141
|
+
orderId: string,
|
|
142
|
+
scope: { tenantId: string; organizationId: string },
|
|
143
|
+
): Promise<Map<string, number>> => {
|
|
144
|
+
const shipments = await findWithDecryption(
|
|
145
|
+
em,
|
|
146
|
+
SalesShipment,
|
|
147
|
+
{ order: orderId, deletedAt: null },
|
|
148
|
+
{},
|
|
149
|
+
scope,
|
|
150
|
+
)
|
|
151
|
+
const shippedByLine = new Map<string, number>()
|
|
152
|
+
if (!shipments.length) return shippedByLine
|
|
153
|
+
const items = await findWithDecryption(
|
|
154
|
+
em,
|
|
155
|
+
SalesShipmentItem,
|
|
156
|
+
{ shipment: { $in: shipments.map((shipment) => shipment.id) } },
|
|
157
|
+
{},
|
|
158
|
+
scope,
|
|
159
|
+
)
|
|
160
|
+
items.forEach((item) => {
|
|
161
|
+
const orderLineId = typeof item.orderLine === 'string' ? item.orderLine : (item.orderLine?.id ?? null)
|
|
162
|
+
if (!orderLineId) return
|
|
163
|
+
const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity)
|
|
164
|
+
shippedByLine.set(orderLineId, next)
|
|
165
|
+
})
|
|
166
|
+
return shippedByLine
|
|
167
|
+
}
|
|
168
|
+
|
|
139
169
|
export type { ShipmentItemSnapshot } from './types'
|