@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960
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/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/translations.js +9 -0
- package/dist/modules/staff/translations.js.map +7 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
- package/dist/modules/translations/lib/extract-record-id.js +31 -2
- package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +3 -0
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/dist/modules/translations/widgets/injection-table.js +18 -29
- package/dist/modules/translations/widgets/injection-table.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/staff/backend/staff/team-members/[id]/page.tsx +8 -0
- package/src/modules/staff/translations.ts +5 -0
- package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
- package/src/modules/translations/lib/extract-record-id.ts +47 -3
- package/src/modules/translations/lib/resolve-field-list.ts +4 -0
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
- package/src/modules/translations/widgets/injection-table.ts +19 -33
- 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,9 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildChanges,
|
|
6
|
+
emitCrudSideEffects,
|
|
7
|
+
requireId
|
|
8
|
+
} from "@open-mercato/shared/lib/commands/helpers";
|
|
5
9
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
deriveResourceFromCommandId,
|
|
12
|
+
invalidateCrudCache
|
|
13
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
7
14
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
15
|
import { resolveNotificationService } from "../../notifications/lib/notificationService.js";
|
|
9
16
|
import { buildFeatureNotificationFromType } from "../../notifications/lib/notificationBuilder.js";
|
|
@@ -11,7 +18,7 @@ import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/h
|
|
|
11
18
|
import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
12
19
|
import { normalizeCustomFieldValues } from "@open-mercato/shared/lib/custom-fields/normalize";
|
|
13
20
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
14
|
-
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
21
|
+
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
15
22
|
import {
|
|
16
23
|
SalesQuote,
|
|
17
24
|
SalesQuoteLine,
|
|
@@ -32,6 +39,11 @@ import {
|
|
|
32
39
|
SalesDocumentTag,
|
|
33
40
|
SalesDocumentTagAssignment
|
|
34
41
|
} from "../data/entities.js";
|
|
42
|
+
import {
|
|
43
|
+
CatalogProduct,
|
|
44
|
+
CatalogProductUnitConversion
|
|
45
|
+
} from "../../catalog/data/entities.js";
|
|
46
|
+
import { Dictionary, DictionaryEntry } from "../../dictionaries/data/entities.js";
|
|
35
47
|
import { CustomFieldValue } from "@open-mercato/core/modules/entities/data/entities";
|
|
36
48
|
import {
|
|
37
49
|
CustomerAddress,
|
|
@@ -64,6 +76,11 @@ import { resolveDictionaryEntryValue } from "../lib/dictionaries.js";
|
|
|
64
76
|
import { resolveStatusEntryIdByValue } from "../lib/statusHelpers.js";
|
|
65
77
|
import { loadSalesSettings } from "./settings.js";
|
|
66
78
|
import { notificationTypes } from "../notifications.js";
|
|
79
|
+
import {
|
|
80
|
+
REFERENCE_UNIT_CODES,
|
|
81
|
+
canonicalizeUnitCode,
|
|
82
|
+
toUnitLookupKey
|
|
83
|
+
} from "@open-mercato/shared/lib/units/unitCodes";
|
|
67
84
|
const orderCrudEvents = {
|
|
68
85
|
module: "sales",
|
|
69
86
|
entity: "order",
|
|
@@ -85,7 +102,9 @@ const quoteCrudEvents = {
|
|
|
85
102
|
})
|
|
86
103
|
};
|
|
87
104
|
const currencyCodeSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3}$/, { message: "currency_code_invalid" });
|
|
88
|
-
const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message: "invalid_date" }).refine((value) => !Number.isNaN(new Date(value).getTime()), {
|
|
105
|
+
const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message: "invalid_date" }).refine((value) => !Number.isNaN(new Date(value).getTime()), {
|
|
106
|
+
message: "invalid_date"
|
|
107
|
+
});
|
|
89
108
|
const addressSnapshotSchema = z.record(z.string(), z.unknown()).nullable().optional();
|
|
90
109
|
const documentUpdateSchema = z.object({
|
|
91
110
|
id: z.string().uuid(),
|
|
@@ -259,13 +278,20 @@ function resolveNoteAuthorFromAuth(auth) {
|
|
|
259
278
|
return uuidRegex.test(sub) ? sub : null;
|
|
260
279
|
}
|
|
261
280
|
function resolveStatusChangeActor(auth, translate) {
|
|
262
|
-
const unknownLabel = translate(
|
|
281
|
+
const unknownLabel = translate(
|
|
282
|
+
"sales.orders.status_change.actor_unknown",
|
|
283
|
+
"unknown user"
|
|
284
|
+
);
|
|
263
285
|
if (!auth) return unknownLabel;
|
|
264
286
|
if (auth.isApiKey) {
|
|
265
287
|
const keyName = typeof auth.keyName === "string" ? auth.keyName.trim() : "";
|
|
266
288
|
const keyId = typeof auth.keyId === "string" ? auth.keyId.trim() : "";
|
|
267
289
|
const label = keyName || keyId || (typeof auth.sub === "string" ? auth.sub : "");
|
|
268
|
-
return label ? translate(
|
|
290
|
+
return label ? translate(
|
|
291
|
+
"sales.orders.status_change.actor_api_key",
|
|
292
|
+
"API key {name}",
|
|
293
|
+
{ name: label }
|
|
294
|
+
) : unknownLabel;
|
|
269
295
|
}
|
|
270
296
|
const email = typeof auth.email === "string" ? auth.email.trim() : "";
|
|
271
297
|
if (email) return email;
|
|
@@ -415,17 +441,29 @@ async function applyDocumentUpdate({
|
|
|
415
441
|
deletedAt: null
|
|
416
442
|
});
|
|
417
443
|
if (!channel) {
|
|
418
|
-
throw new CrudHttpError(400, {
|
|
444
|
+
throw new CrudHttpError(400, {
|
|
445
|
+
error: translate(
|
|
446
|
+
"sales.documents.detail.channelInvalid",
|
|
447
|
+
"Selected channel could not be found."
|
|
448
|
+
)
|
|
449
|
+
});
|
|
419
450
|
}
|
|
420
451
|
entity.channelId = channel.id;
|
|
421
452
|
}
|
|
422
453
|
}
|
|
423
454
|
if (input.statusEntryId !== void 0) {
|
|
424
|
-
const statusValue = await resolveDictionaryEntryValue(
|
|
455
|
+
const statusValue = await resolveDictionaryEntryValue(
|
|
456
|
+
em,
|
|
457
|
+
input.statusEntryId
|
|
458
|
+
);
|
|
425
459
|
if (input.statusEntryId && !statusValue) {
|
|
426
|
-
throw new CrudHttpError(400, {
|
|
460
|
+
throw new CrudHttpError(400, {
|
|
461
|
+
error: translate(
|
|
462
|
+
"sales.documents.detail.statusInvalid",
|
|
463
|
+
"Selected status could not be found."
|
|
464
|
+
)
|
|
465
|
+
});
|
|
427
466
|
}
|
|
428
|
-
;
|
|
429
467
|
entity.statusEntryId = input.statusEntryId ?? null;
|
|
430
468
|
entity.status = statusValue;
|
|
431
469
|
}
|
|
@@ -448,13 +486,23 @@ async function applyDocumentUpdate({
|
|
|
448
486
|
if (input.shippingAddressId !== void 0) {
|
|
449
487
|
entity.shippingAddressId = input.shippingAddressId ?? null;
|
|
450
488
|
if (input.shippingAddressSnapshot === void 0) {
|
|
451
|
-
entity.shippingAddressSnapshot = await resolveAddressSnapshot(
|
|
489
|
+
entity.shippingAddressSnapshot = await resolveAddressSnapshot(
|
|
490
|
+
em,
|
|
491
|
+
organizationId,
|
|
492
|
+
tenantId,
|
|
493
|
+
input.shippingAddressId
|
|
494
|
+
);
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
497
|
if (input.billingAddressId !== void 0) {
|
|
455
498
|
entity.billingAddressId = input.billingAddressId ?? null;
|
|
456
499
|
if (input.billingAddressSnapshot === void 0) {
|
|
457
|
-
entity.billingAddressSnapshot = await resolveAddressSnapshot(
|
|
500
|
+
entity.billingAddressSnapshot = await resolveAddressSnapshot(
|
|
501
|
+
em,
|
|
502
|
+
organizationId,
|
|
503
|
+
tenantId,
|
|
504
|
+
input.billingAddressId
|
|
505
|
+
);
|
|
458
506
|
}
|
|
459
507
|
}
|
|
460
508
|
if (input.shippingAddressSnapshot !== void 0) {
|
|
@@ -473,18 +521,20 @@ async function applyDocumentUpdate({
|
|
|
473
521
|
deletedAt: null
|
|
474
522
|
});
|
|
475
523
|
if (!shippingMethod) {
|
|
476
|
-
throw new CrudHttpError(400, {
|
|
524
|
+
throw new CrudHttpError(400, {
|
|
525
|
+
error: translate(
|
|
526
|
+
"sales.documents.detail.shippingMethodInvalid",
|
|
527
|
+
"Selected shipping method could not be found."
|
|
528
|
+
)
|
|
529
|
+
});
|
|
477
530
|
}
|
|
478
531
|
}
|
|
479
|
-
;
|
|
480
532
|
entity.shippingMethodId = input.shippingMethodId ?? null;
|
|
481
533
|
entity.shippingMethod = shippingMethod ?? null;
|
|
482
534
|
entity.shippingMethodCode = input.shippingMethodCode ?? shippingMethod?.code ?? null;
|
|
483
535
|
if (input.shippingMethodSnapshot !== void 0) {
|
|
484
|
-
;
|
|
485
536
|
entity.shippingMethodSnapshot = input.shippingMethodSnapshot ?? null;
|
|
486
537
|
} else {
|
|
487
|
-
;
|
|
488
538
|
entity.shippingMethodSnapshot = shippingMethod ? {
|
|
489
539
|
id: shippingMethod.id,
|
|
490
540
|
code: shippingMethod.code,
|
|
@@ -514,18 +564,20 @@ async function applyDocumentUpdate({
|
|
|
514
564
|
deletedAt: null
|
|
515
565
|
});
|
|
516
566
|
if (!paymentMethod) {
|
|
517
|
-
throw new CrudHttpError(400, {
|
|
567
|
+
throw new CrudHttpError(400, {
|
|
568
|
+
error: translate(
|
|
569
|
+
"sales.documents.detail.paymentMethodInvalid",
|
|
570
|
+
"Selected payment method could not be found."
|
|
571
|
+
)
|
|
572
|
+
});
|
|
518
573
|
}
|
|
519
574
|
}
|
|
520
|
-
;
|
|
521
575
|
entity.paymentMethodId = input.paymentMethodId ?? null;
|
|
522
576
|
entity.paymentMethod = paymentMethod ?? null;
|
|
523
577
|
entity.paymentMethodCode = input.paymentMethodCode ?? paymentMethod?.code ?? null;
|
|
524
578
|
if (input.paymentMethodSnapshot !== void 0) {
|
|
525
|
-
;
|
|
526
579
|
entity.paymentMethodSnapshot = input.paymentMethodSnapshot ?? null;
|
|
527
580
|
} else {
|
|
528
|
-
;
|
|
529
581
|
entity.paymentMethodSnapshot = paymentMethod ? {
|
|
530
582
|
id: paymentMethod.id,
|
|
531
583
|
code: paymentMethod.code,
|
|
@@ -550,7 +602,6 @@ async function applyDocumentUpdate({
|
|
|
550
602
|
});
|
|
551
603
|
}
|
|
552
604
|
if (input.customFieldSetId !== void 0) {
|
|
553
|
-
;
|
|
554
605
|
entity.customFieldSetId = input.customFieldSetId ?? null;
|
|
555
606
|
}
|
|
556
607
|
if (input.customFields !== void 0) {
|
|
@@ -567,9 +618,24 @@ async function applyDocumentUpdate({
|
|
|
567
618
|
async function loadQuoteSnapshot(em, id) {
|
|
568
619
|
const quote = await em.findOne(SalesQuote, { id, deletedAt: null });
|
|
569
620
|
if (!quote) return null;
|
|
570
|
-
const lines = await em.find(
|
|
571
|
-
|
|
572
|
-
|
|
621
|
+
const lines = await em.find(
|
|
622
|
+
SalesQuoteLine,
|
|
623
|
+
{ quote },
|
|
624
|
+
{ orderBy: { lineNumber: "asc" } }
|
|
625
|
+
);
|
|
626
|
+
const adjustments = await em.find(
|
|
627
|
+
SalesQuoteAdjustment,
|
|
628
|
+
{ quote },
|
|
629
|
+
{ orderBy: { position: "asc" } }
|
|
630
|
+
);
|
|
631
|
+
const [
|
|
632
|
+
addresses,
|
|
633
|
+
notes,
|
|
634
|
+
tags,
|
|
635
|
+
quoteCustomFields,
|
|
636
|
+
lineCustomFields,
|
|
637
|
+
adjustmentCustomFields
|
|
638
|
+
] = await Promise.all([
|
|
573
639
|
em.find(SalesDocumentAddress, { documentId: id, documentKind: "quote" }),
|
|
574
640
|
em.find(SalesNote, { contextType: "quote", contextId: id }),
|
|
575
641
|
findWithDecryption(
|
|
@@ -590,38 +656,48 @@ async function loadQuoteSnapshot(em, id) {
|
|
|
590
656
|
em,
|
|
591
657
|
entityId: E.sales.sales_quote_line,
|
|
592
658
|
recordIds: lines.map((line) => line.id),
|
|
593
|
-
tenantIdByRecord: Object.fromEntries(
|
|
594
|
-
|
|
659
|
+
tenantIdByRecord: Object.fromEntries(
|
|
660
|
+
lines.map((line) => [line.id, quote.tenantId])
|
|
661
|
+
),
|
|
662
|
+
organizationIdByRecord: Object.fromEntries(
|
|
663
|
+
lines.map((line) => [line.id, quote.organizationId])
|
|
664
|
+
)
|
|
595
665
|
}) : Promise.resolve({}),
|
|
596
666
|
adjustments.length ? loadCustomFieldValues({
|
|
597
667
|
em,
|
|
598
668
|
entityId: E.sales.sales_quote_adjustment,
|
|
599
669
|
recordIds: adjustments.map((adj) => adj.id),
|
|
600
|
-
tenantIdByRecord: Object.fromEntries(
|
|
601
|
-
|
|
670
|
+
tenantIdByRecord: Object.fromEntries(
|
|
671
|
+
adjustments.map((adj) => [adj.id, quote.tenantId])
|
|
672
|
+
),
|
|
673
|
+
organizationIdByRecord: Object.fromEntries(
|
|
674
|
+
adjustments.map((adj) => [adj.id, quote.organizationId])
|
|
675
|
+
)
|
|
602
676
|
}) : Promise.resolve({})
|
|
603
677
|
]);
|
|
604
|
-
const addressSnapshots = addresses.map(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
678
|
+
const addressSnapshots = addresses.map(
|
|
679
|
+
(entry) => ({
|
|
680
|
+
id: entry.id,
|
|
681
|
+
organizationId: entry.organizationId,
|
|
682
|
+
tenantId: entry.tenantId,
|
|
683
|
+
documentId: entry.documentId,
|
|
684
|
+
documentKind: "quote",
|
|
685
|
+
customerAddressId: entry.customerAddressId ?? null,
|
|
686
|
+
name: entry.name ?? null,
|
|
687
|
+
purpose: entry.purpose ?? null,
|
|
688
|
+
companyName: entry.companyName ?? null,
|
|
689
|
+
addressLine1: entry.addressLine1,
|
|
690
|
+
addressLine2: entry.addressLine2 ?? null,
|
|
691
|
+
city: entry.city ?? null,
|
|
692
|
+
region: entry.region ?? null,
|
|
693
|
+
postalCode: entry.postalCode ?? null,
|
|
694
|
+
country: entry.country ?? null,
|
|
695
|
+
buildingNumber: entry.buildingNumber ?? null,
|
|
696
|
+
flatNumber: entry.flatNumber ?? null,
|
|
697
|
+
latitude: entry.latitude ?? null,
|
|
698
|
+
longitude: entry.longitude ?? null
|
|
699
|
+
})
|
|
700
|
+
);
|
|
625
701
|
const noteSnapshots = notes.map((entry) => ({
|
|
626
702
|
id: entry.id,
|
|
627
703
|
organizationId: entry.organizationId,
|
|
@@ -703,6 +779,9 @@ async function loadQuoteSnapshot(em, id) {
|
|
|
703
779
|
comment: line.comment ?? null,
|
|
704
780
|
quantity: line.quantity,
|
|
705
781
|
quantityUnit: line.quantityUnit ?? null,
|
|
782
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
783
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
784
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
706
785
|
currencyCode: line.currencyCode,
|
|
707
786
|
unitPriceNet: line.unitPriceNet,
|
|
708
787
|
unitPriceGross: line.unitPriceGross,
|
|
@@ -744,9 +823,26 @@ async function loadQuoteSnapshot(em, id) {
|
|
|
744
823
|
async function loadOrderSnapshot(em, id) {
|
|
745
824
|
const order = await em.findOne(SalesOrder, { id, deletedAt: null });
|
|
746
825
|
if (!order) return null;
|
|
747
|
-
const lines = await em.find(
|
|
748
|
-
|
|
749
|
-
|
|
826
|
+
const lines = await em.find(
|
|
827
|
+
SalesOrderLine,
|
|
828
|
+
{ order },
|
|
829
|
+
{ orderBy: { lineNumber: "asc" } }
|
|
830
|
+
);
|
|
831
|
+
const adjustments = await em.find(
|
|
832
|
+
SalesOrderAdjustment,
|
|
833
|
+
{ order },
|
|
834
|
+
{ orderBy: { position: "asc" } }
|
|
835
|
+
);
|
|
836
|
+
const [
|
|
837
|
+
addresses,
|
|
838
|
+
notes,
|
|
839
|
+
tags,
|
|
840
|
+
shipments,
|
|
841
|
+
payments,
|
|
842
|
+
orderCustomFields,
|
|
843
|
+
lineCustomFields,
|
|
844
|
+
adjustmentCustomFields
|
|
845
|
+
] = await Promise.all([
|
|
750
846
|
em.find(SalesDocumentAddress, { documentId: id, documentKind: "order" }),
|
|
751
847
|
em.find(SalesNote, { contextType: "order", contextId: id }),
|
|
752
848
|
findWithDecryption(
|
|
@@ -769,40 +865,54 @@ async function loadOrderSnapshot(em, id) {
|
|
|
769
865
|
em,
|
|
770
866
|
entityId: E.sales.sales_order_line,
|
|
771
867
|
recordIds: lines.map((line) => line.id),
|
|
772
|
-
tenantIdByRecord: Object.fromEntries(
|
|
773
|
-
|
|
868
|
+
tenantIdByRecord: Object.fromEntries(
|
|
869
|
+
lines.map((line) => [line.id, order.tenantId])
|
|
870
|
+
),
|
|
871
|
+
organizationIdByRecord: Object.fromEntries(
|
|
872
|
+
lines.map((line) => [line.id, order.organizationId])
|
|
873
|
+
)
|
|
774
874
|
}) : Promise.resolve({}),
|
|
775
875
|
adjustments.length ? loadCustomFieldValues({
|
|
776
876
|
em,
|
|
777
877
|
entityId: E.sales.sales_order_adjustment,
|
|
778
878
|
recordIds: adjustments.map((adj) => adj.id),
|
|
779
|
-
tenantIdByRecord: Object.fromEntries(
|
|
780
|
-
|
|
879
|
+
tenantIdByRecord: Object.fromEntries(
|
|
880
|
+
adjustments.map((adj) => [adj.id, order.tenantId])
|
|
881
|
+
),
|
|
882
|
+
organizationIdByRecord: Object.fromEntries(
|
|
883
|
+
adjustments.map((adj) => [adj.id, order.organizationId])
|
|
884
|
+
)
|
|
781
885
|
}) : Promise.resolve({})
|
|
782
886
|
]);
|
|
783
|
-
const shipmentSnapshots = (await Promise.all(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
887
|
+
const shipmentSnapshots = (await Promise.all(
|
|
888
|
+
shipments.map((entry) => loadShipmentSnapshot(em, entry.id))
|
|
889
|
+
)).filter((entry) => !!entry);
|
|
890
|
+
const paymentSnapshots = (await Promise.all(
|
|
891
|
+
payments.map((entry) => loadPaymentSnapshot(em, entry.id))
|
|
892
|
+
)).filter((entry) => !!entry);
|
|
893
|
+
const addressSnapshots = addresses.map(
|
|
894
|
+
(entry) => ({
|
|
895
|
+
id: entry.id,
|
|
896
|
+
organizationId: entry.organizationId,
|
|
897
|
+
tenantId: entry.tenantId,
|
|
898
|
+
documentId: entry.documentId,
|
|
899
|
+
documentKind: "order",
|
|
900
|
+
customerAddressId: entry.customerAddressId ?? null,
|
|
901
|
+
name: entry.name ?? null,
|
|
902
|
+
purpose: entry.purpose ?? null,
|
|
903
|
+
companyName: entry.companyName ?? null,
|
|
904
|
+
addressLine1: entry.addressLine1,
|
|
905
|
+
addressLine2: entry.addressLine2 ?? null,
|
|
906
|
+
city: entry.city ?? null,
|
|
907
|
+
region: entry.region ?? null,
|
|
908
|
+
postalCode: entry.postalCode ?? null,
|
|
909
|
+
country: entry.country ?? null,
|
|
910
|
+
buildingNumber: entry.buildingNumber ?? null,
|
|
911
|
+
flatNumber: entry.flatNumber ?? null,
|
|
912
|
+
latitude: entry.latitude ?? null,
|
|
913
|
+
longitude: entry.longitude ?? null
|
|
914
|
+
})
|
|
915
|
+
);
|
|
806
916
|
const noteSnapshots = notes.map((entry) => ({
|
|
807
917
|
id: entry.id,
|
|
808
918
|
organizationId: entry.organizationId,
|
|
@@ -899,6 +1009,9 @@ async function loadOrderSnapshot(em, id) {
|
|
|
899
1009
|
comment: line.comment ?? null,
|
|
900
1010
|
quantity: line.quantity,
|
|
901
1011
|
quantityUnit: line.quantityUnit ?? null,
|
|
1012
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
1013
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
1014
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
902
1015
|
reservedQuantity: line.reservedQuantity,
|
|
903
1016
|
fulfilledQuantity: line.fulfilledQuantity,
|
|
904
1017
|
invoicedQuantity: line.invoicedQuantity,
|
|
@@ -975,6 +1088,391 @@ function toNumeric(value) {
|
|
|
975
1088
|
}
|
|
976
1089
|
return 0;
|
|
977
1090
|
}
|
|
1091
|
+
const UNIT_DICTIONARY_KEYS = ["unit", "units", "measurement_units"];
|
|
1092
|
+
const UOM_REFERENCE_UNITS = new Set(REFERENCE_UNIT_CODES);
|
|
1093
|
+
const UOM_NORMALIZED_SCALE = 6;
|
|
1094
|
+
const UOM_NORMALIZED_MAX = 1e12;
|
|
1095
|
+
const UNIT_PRICE_AUTOCONVERT_SCALE = 4;
|
|
1096
|
+
function createUomResolver() {
|
|
1097
|
+
return {
|
|
1098
|
+
dictionaryPromise: null,
|
|
1099
|
+
unitExistsCache: /* @__PURE__ */ new Map(),
|
|
1100
|
+
productCache: /* @__PURE__ */ new Map()
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function normalizeUnitCode(value) {
|
|
1104
|
+
return canonicalizeUnitCode(value);
|
|
1105
|
+
}
|
|
1106
|
+
function unitLookupKey(value) {
|
|
1107
|
+
return toUnitLookupKey(value);
|
|
1108
|
+
}
|
|
1109
|
+
function roundNormalizedQuantity(value) {
|
|
1110
|
+
const factor = 10 ** UOM_NORMALIZED_SCALE;
|
|
1111
|
+
if (!Number.isFinite(value)) return 0;
|
|
1112
|
+
return Math.round(value * factor) / factor;
|
|
1113
|
+
}
|
|
1114
|
+
function assertNormalizedPrecision(value) {
|
|
1115
|
+
if (!Number.isFinite(value) || Math.abs(value) >= UOM_NORMALIZED_MAX) {
|
|
1116
|
+
throw new CrudHttpError(422, { error: "uom.precision_overflow" });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function toOptionalNumber(value) {
|
|
1120
|
+
if (value === null || value === void 0) return null;
|
|
1121
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1122
|
+
if (typeof value === "string" && value.trim().length) {
|
|
1123
|
+
const parsed = Number(value);
|
|
1124
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
function resolveSnapshotToBaseFactor(snapshot) {
|
|
1129
|
+
if (!snapshot || typeof snapshot !== "object") return 1;
|
|
1130
|
+
const payload = snapshot;
|
|
1131
|
+
const factor = toOptionalNumber(payload.toBaseFactor ?? payload.to_base_factor);
|
|
1132
|
+
if (!Number.isFinite(factor) || !factor || factor <= 0) return 1;
|
|
1133
|
+
return factor;
|
|
1134
|
+
}
|
|
1135
|
+
function roundAutoConvertedUnitPrice(value) {
|
|
1136
|
+
if (!Number.isFinite(value) || value < 0) return null;
|
|
1137
|
+
const factor = 10 ** UNIT_PRICE_AUTOCONVERT_SCALE;
|
|
1138
|
+
const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
|
|
1139
|
+
return Number.isFinite(rounded) ? rounded : null;
|
|
1140
|
+
}
|
|
1141
|
+
function convertLineUnitPricesOnUnitChange(params) {
|
|
1142
|
+
if (!params.existingSnapshot) {
|
|
1143
|
+
return {
|
|
1144
|
+
unitPriceNet: params.unitPriceNet,
|
|
1145
|
+
unitPriceGross: params.unitPriceGross,
|
|
1146
|
+
didConvert: false
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const existingUnitPriceNet = typeof params.existingSnapshot.unitPriceNet === "number" && Number.isFinite(params.existingSnapshot.unitPriceNet) ? params.existingSnapshot.unitPriceNet : null;
|
|
1150
|
+
const existingUnitPriceGross = typeof params.existingSnapshot.unitPriceGross === "number" && Number.isFinite(params.existingSnapshot.unitPriceGross) ? params.existingSnapshot.unitPriceGross : null;
|
|
1151
|
+
const sameNumber = (left, right) => {
|
|
1152
|
+
if (left === null || right === null) return left === right;
|
|
1153
|
+
return Math.abs(left - right) < 1e-6;
|
|
1154
|
+
};
|
|
1155
|
+
const shouldConvertNet = params.unitPriceNet === null || sameNumber(params.unitPriceNet, existingUnitPriceNet);
|
|
1156
|
+
const shouldConvertGross = params.unitPriceGross === null || sameNumber(params.unitPriceGross, existingUnitPriceGross);
|
|
1157
|
+
if (!shouldConvertNet && !shouldConvertGross) {
|
|
1158
|
+
return {
|
|
1159
|
+
unitPriceNet: params.unitPriceNet,
|
|
1160
|
+
unitPriceGross: params.unitPriceGross,
|
|
1161
|
+
didConvert: false
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
const previousUnit = normalizeUnitCode(params.existingSnapshot?.quantityUnit ?? null);
|
|
1165
|
+
const nextUnit = normalizeUnitCode(params.nextQuantityUnit);
|
|
1166
|
+
if (!previousUnit || !nextUnit || previousUnit === nextUnit) {
|
|
1167
|
+
return {
|
|
1168
|
+
unitPriceNet: params.unitPriceNet,
|
|
1169
|
+
unitPriceGross: params.unitPriceGross,
|
|
1170
|
+
didConvert: false
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const previousFactor = resolveSnapshotToBaseFactor(
|
|
1174
|
+
params.existingSnapshot?.uomSnapshot ?? null
|
|
1175
|
+
);
|
|
1176
|
+
const nextFactor = resolveSnapshotToBaseFactor(params.nextUomSnapshot);
|
|
1177
|
+
if (!Number.isFinite(previousFactor) || previousFactor <= 0 || !Number.isFinite(nextFactor) || nextFactor <= 0) {
|
|
1178
|
+
return {
|
|
1179
|
+
unitPriceNet: params.unitPriceNet,
|
|
1180
|
+
unitPriceGross: params.unitPriceGross,
|
|
1181
|
+
didConvert: false
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
const convertAmount = (value, shouldConvert) => {
|
|
1185
|
+
if (!shouldConvert) return value;
|
|
1186
|
+
if (value === null || !Number.isFinite(value)) return value;
|
|
1187
|
+
const converted = roundAutoConvertedUnitPrice(value / previousFactor * nextFactor);
|
|
1188
|
+
return converted ?? value;
|
|
1189
|
+
};
|
|
1190
|
+
const nextUnitPriceNet = convertAmount(params.unitPriceNet, shouldConvertNet);
|
|
1191
|
+
const nextUnitPriceGross = convertAmount(
|
|
1192
|
+
params.unitPriceGross,
|
|
1193
|
+
shouldConvertGross
|
|
1194
|
+
);
|
|
1195
|
+
const didConvert = nextUnitPriceNet !== params.unitPriceNet || nextUnitPriceGross !== params.unitPriceGross;
|
|
1196
|
+
return {
|
|
1197
|
+
unitPriceNet: nextUnitPriceNet,
|
|
1198
|
+
unitPriceGross: nextUnitPriceGross,
|
|
1199
|
+
didConvert
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function resolveReferenceUnit(value) {
|
|
1203
|
+
const normalized = toUnitLookupKey(value);
|
|
1204
|
+
if (!normalized) return null;
|
|
1205
|
+
if (!UOM_REFERENCE_UNITS.has(normalized)) return null;
|
|
1206
|
+
return normalized;
|
|
1207
|
+
}
|
|
1208
|
+
async function resolveUnitDictionaryScoped(em, organizationId, tenantId) {
|
|
1209
|
+
return findOneWithDecryption(
|
|
1210
|
+
em,
|
|
1211
|
+
Dictionary,
|
|
1212
|
+
{
|
|
1213
|
+
organizationId,
|
|
1214
|
+
tenantId,
|
|
1215
|
+
key: { $in: [...UNIT_DICTIONARY_KEYS] },
|
|
1216
|
+
deletedAt: null,
|
|
1217
|
+
isActive: true
|
|
1218
|
+
},
|
|
1219
|
+
{ orderBy: { createdAt: "asc" } }
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
async function assertUnitExists(em, resolver, organizationId, tenantId, unitCode) {
|
|
1223
|
+
const normalizedCode = unitLookupKey(unitCode);
|
|
1224
|
+
if (!normalizedCode) return;
|
|
1225
|
+
const rawUnitCode = unitCode.trim();
|
|
1226
|
+
if (!resolver.dictionaryPromise) {
|
|
1227
|
+
resolver.dictionaryPromise = resolveUnitDictionaryScoped(
|
|
1228
|
+
em,
|
|
1229
|
+
organizationId,
|
|
1230
|
+
tenantId
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
const dictionary = await resolver.dictionaryPromise;
|
|
1234
|
+
if (!dictionary) {
|
|
1235
|
+
throw new CrudHttpError(400, { error: "uom.unit_not_found" });
|
|
1236
|
+
}
|
|
1237
|
+
const cacheKey = `${dictionary.id}:${normalizedCode}`;
|
|
1238
|
+
if (resolver.unitExistsCache.has(cacheKey)) {
|
|
1239
|
+
if (!resolver.unitExistsCache.get(cacheKey)) {
|
|
1240
|
+
throw new CrudHttpError(400, { error: "uom.unit_not_found" });
|
|
1241
|
+
}
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const entry = await findOneWithDecryption(em, DictionaryEntry, {
|
|
1245
|
+
dictionary,
|
|
1246
|
+
organizationId: dictionary.organizationId,
|
|
1247
|
+
tenantId: dictionary.tenantId,
|
|
1248
|
+
$or: [{ normalizedValue: normalizedCode }, { value: rawUnitCode }]
|
|
1249
|
+
});
|
|
1250
|
+
const exists = !!entry;
|
|
1251
|
+
resolver.unitExistsCache.set(cacheKey, exists);
|
|
1252
|
+
if (!exists) {
|
|
1253
|
+
throw new CrudHttpError(400, { error: "uom.unit_not_found" });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function resolveProductUomState(em, resolver, organizationId, tenantId, productId) {
|
|
1257
|
+
if (resolver.productCache.has(productId)) {
|
|
1258
|
+
return resolver.productCache.get(productId) ?? null;
|
|
1259
|
+
}
|
|
1260
|
+
const product = await findOneWithDecryption(em, CatalogProduct, {
|
|
1261
|
+
id: productId,
|
|
1262
|
+
organizationId,
|
|
1263
|
+
tenantId,
|
|
1264
|
+
deletedAt: null
|
|
1265
|
+
});
|
|
1266
|
+
if (!product) {
|
|
1267
|
+
resolver.productCache.set(productId, null);
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
const conversions = await findWithDecryption(em, CatalogProductUnitConversion, {
|
|
1271
|
+
product: product.id,
|
|
1272
|
+
organizationId,
|
|
1273
|
+
tenantId,
|
|
1274
|
+
deletedAt: null,
|
|
1275
|
+
isActive: true
|
|
1276
|
+
});
|
|
1277
|
+
const conversionsByUnitKey = /* @__PURE__ */ new Map();
|
|
1278
|
+
for (const conversion of conversions) {
|
|
1279
|
+
const key = unitLookupKey(conversion.unitCode);
|
|
1280
|
+
if (!key) continue;
|
|
1281
|
+
conversionsByUnitKey.set(key, conversion);
|
|
1282
|
+
}
|
|
1283
|
+
const state = {
|
|
1284
|
+
productId: product.id,
|
|
1285
|
+
baseUnitCode: normalizeUnitCode(product.defaultUnit ?? null),
|
|
1286
|
+
defaultSalesUnit: normalizeUnitCode(product.defaultSalesUnit ?? null),
|
|
1287
|
+
unitPriceEnabled: Boolean(product.unitPriceEnabled),
|
|
1288
|
+
unitPriceReferenceUnit: resolveReferenceUnit(
|
|
1289
|
+
product.unitPriceReferenceUnit
|
|
1290
|
+
),
|
|
1291
|
+
unitPriceBaseQuantity: product.unitPriceBaseQuantity ?? null,
|
|
1292
|
+
conversionsByUnitKey
|
|
1293
|
+
};
|
|
1294
|
+
resolver.productCache.set(productId, state);
|
|
1295
|
+
return state;
|
|
1296
|
+
}
|
|
1297
|
+
function buildUnitPriceReferenceSnapshot(params) {
|
|
1298
|
+
if (!params.product.unitPriceEnabled) return void 0;
|
|
1299
|
+
const baseQuantityNumber = toNumeric(params.product.unitPriceBaseQuantity);
|
|
1300
|
+
const output = {
|
|
1301
|
+
enabled: true,
|
|
1302
|
+
referenceUnitCode: params.product.unitPriceReferenceUnit ?? null,
|
|
1303
|
+
baseQuantity: params.product.unitPriceBaseQuantity ?? null
|
|
1304
|
+
};
|
|
1305
|
+
if (!params.product.unitPriceReferenceUnit || params.toBaseFactor <= 0 || baseQuantityNumber <= 0) {
|
|
1306
|
+
return output;
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof params.unitPriceGross === "number" && Number.isFinite(params.unitPriceGross)) {
|
|
1309
|
+
output.grossPerReference = toNumericString(
|
|
1310
|
+
params.unitPriceGross / params.toBaseFactor * baseQuantityNumber
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof params.unitPriceNet === "number" && Number.isFinite(params.unitPriceNet)) {
|
|
1314
|
+
output.netPerReference = toNumericString(
|
|
1315
|
+
params.unitPriceNet / params.toBaseFactor * baseQuantityNumber
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
return output;
|
|
1319
|
+
}
|
|
1320
|
+
async function normalizeLineUom(input) {
|
|
1321
|
+
const quantity = toNumeric(input.line.quantity);
|
|
1322
|
+
const existingSnapshot = input.line.uomSnapshot && typeof input.line.uomSnapshot === "object" ? cloneJson(input.line.uomSnapshot) : null;
|
|
1323
|
+
const productId = typeof input.line.productId === "string" ? input.line.productId : null;
|
|
1324
|
+
const variantId = typeof input.line.productVariantId === "string" ? input.line.productVariantId : null;
|
|
1325
|
+
const enteredUnitInput = normalizeUnitCode(input.line.quantityUnit);
|
|
1326
|
+
if (!productId) {
|
|
1327
|
+
if (enteredUnitInput) {
|
|
1328
|
+
await assertUnitExists(
|
|
1329
|
+
input.em,
|
|
1330
|
+
input.resolver,
|
|
1331
|
+
input.organizationId,
|
|
1332
|
+
input.tenantId,
|
|
1333
|
+
enteredUnitInput
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
const normalizedQuantity2 = toNumeric(
|
|
1337
|
+
input.line.normalizedQuantity ?? quantity
|
|
1338
|
+
);
|
|
1339
|
+
assertNormalizedPrecision(normalizedQuantity2);
|
|
1340
|
+
return {
|
|
1341
|
+
quantity,
|
|
1342
|
+
quantityUnit: enteredUnitInput ?? null,
|
|
1343
|
+
normalizedQuantity: normalizedQuantity2,
|
|
1344
|
+
normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
|
|
1345
|
+
uomSnapshot: existingSnapshot ?? {
|
|
1346
|
+
version: 1,
|
|
1347
|
+
productId: null,
|
|
1348
|
+
productVariantId: variantId,
|
|
1349
|
+
baseUnitCode: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
|
|
1350
|
+
enteredUnitCode: enteredUnitInput,
|
|
1351
|
+
enteredQuantity: toNumericString(quantity) ?? "0",
|
|
1352
|
+
toBaseFactor: "1",
|
|
1353
|
+
normalizedQuantity: toNumericString(normalizedQuantity2) ?? "0",
|
|
1354
|
+
rounding: { mode: "half_up", scale: UOM_NORMALIZED_SCALE },
|
|
1355
|
+
source: { conversionId: null, resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
const productState = await resolveProductUomState(
|
|
1360
|
+
input.em,
|
|
1361
|
+
input.resolver,
|
|
1362
|
+
input.organizationId,
|
|
1363
|
+
input.tenantId,
|
|
1364
|
+
productId
|
|
1365
|
+
);
|
|
1366
|
+
if (!productState) {
|
|
1367
|
+
if (enteredUnitInput) {
|
|
1368
|
+
await assertUnitExists(
|
|
1369
|
+
input.em,
|
|
1370
|
+
input.resolver,
|
|
1371
|
+
input.organizationId,
|
|
1372
|
+
input.tenantId,
|
|
1373
|
+
enteredUnitInput
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const normalizedQuantity2 = toNumeric(
|
|
1377
|
+
input.line.normalizedQuantity ?? quantity
|
|
1378
|
+
);
|
|
1379
|
+
assertNormalizedPrecision(normalizedQuantity2);
|
|
1380
|
+
return {
|
|
1381
|
+
quantity,
|
|
1382
|
+
quantityUnit: enteredUnitInput ?? null,
|
|
1383
|
+
normalizedQuantity: normalizedQuantity2,
|
|
1384
|
+
normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
|
|
1385
|
+
uomSnapshot: existingSnapshot
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const baseUnitCode = productState.baseUnitCode;
|
|
1389
|
+
const enteredUnitCode = enteredUnitInput ?? productState.defaultSalesUnit ?? baseUnitCode ?? normalizeUnitCode(input.line.normalizedUnit);
|
|
1390
|
+
if (enteredUnitCode) {
|
|
1391
|
+
await assertUnitExists(
|
|
1392
|
+
input.em,
|
|
1393
|
+
input.resolver,
|
|
1394
|
+
input.organizationId,
|
|
1395
|
+
input.tenantId,
|
|
1396
|
+
enteredUnitCode
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
if (baseUnitCode) {
|
|
1400
|
+
await assertUnitExists(
|
|
1401
|
+
input.em,
|
|
1402
|
+
input.resolver,
|
|
1403
|
+
input.organizationId,
|
|
1404
|
+
input.tenantId,
|
|
1405
|
+
baseUnitCode
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (!baseUnitCode) {
|
|
1409
|
+
const requiresBase = Boolean(enteredUnitCode) || productState.conversionsByUnitKey.size > 0 || Boolean(productState.defaultSalesUnit);
|
|
1410
|
+
if (requiresBase) {
|
|
1411
|
+
throw new CrudHttpError(400, { error: "uom.default_unit_missing" });
|
|
1412
|
+
}
|
|
1413
|
+
const normalizedQuantity2 = toNumeric(
|
|
1414
|
+
input.line.normalizedQuantity ?? quantity
|
|
1415
|
+
);
|
|
1416
|
+
assertNormalizedPrecision(normalizedQuantity2);
|
|
1417
|
+
return {
|
|
1418
|
+
quantity,
|
|
1419
|
+
quantityUnit: enteredUnitCode ?? null,
|
|
1420
|
+
normalizedQuantity: normalizedQuantity2,
|
|
1421
|
+
normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitCode ?? null,
|
|
1422
|
+
uomSnapshot: existingSnapshot
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const resolvedEnteredUnit = enteredUnitCode ?? baseUnitCode;
|
|
1426
|
+
const enteredKey = unitLookupKey(resolvedEnteredUnit);
|
|
1427
|
+
const baseKey = unitLookupKey(baseUnitCode);
|
|
1428
|
+
let toBaseFactor = 1;
|
|
1429
|
+
let conversionId = null;
|
|
1430
|
+
if (enteredKey && baseKey && enteredKey !== baseKey) {
|
|
1431
|
+
const conversion = productState.conversionsByUnitKey.get(enteredKey);
|
|
1432
|
+
if (!conversion) {
|
|
1433
|
+
throw new CrudHttpError(400, { error: "uom.conversion_not_found" });
|
|
1434
|
+
}
|
|
1435
|
+
toBaseFactor = toNumeric(conversion.toBaseFactor);
|
|
1436
|
+
conversionId = conversion.id;
|
|
1437
|
+
}
|
|
1438
|
+
if (!Number.isFinite(toBaseFactor) || toBaseFactor <= 0) {
|
|
1439
|
+
throw new CrudHttpError(400, { error: "uom.invalid_factor" });
|
|
1440
|
+
}
|
|
1441
|
+
const normalizedQuantity = roundNormalizedQuantity(quantity * toBaseFactor);
|
|
1442
|
+
assertNormalizedPrecision(normalizedQuantity);
|
|
1443
|
+
const unitPriceReference = buildUnitPriceReferenceSnapshot({
|
|
1444
|
+
product: productState,
|
|
1445
|
+
toBaseFactor,
|
|
1446
|
+
unitPriceNet: toOptionalNumber(input.line.unitPriceNet),
|
|
1447
|
+
unitPriceGross: toOptionalNumber(input.line.unitPriceGross)
|
|
1448
|
+
});
|
|
1449
|
+
const snapshot = {
|
|
1450
|
+
version: 1,
|
|
1451
|
+
productId,
|
|
1452
|
+
productVariantId: variantId,
|
|
1453
|
+
baseUnitCode,
|
|
1454
|
+
enteredUnitCode: resolvedEnteredUnit,
|
|
1455
|
+
enteredQuantity: toNumericString(quantity) ?? "0",
|
|
1456
|
+
toBaseFactor: toNumericString(toBaseFactor) ?? "1",
|
|
1457
|
+
normalizedQuantity: toNumericString(normalizedQuantity) ?? "0",
|
|
1458
|
+
rounding: {
|
|
1459
|
+
mode: "half_up",
|
|
1460
|
+
scale: UOM_NORMALIZED_SCALE
|
|
1461
|
+
},
|
|
1462
|
+
source: {
|
|
1463
|
+
conversionId,
|
|
1464
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1465
|
+
},
|
|
1466
|
+
...unitPriceReference ? { unitPriceReference } : {}
|
|
1467
|
+
};
|
|
1468
|
+
return {
|
|
1469
|
+
quantity,
|
|
1470
|
+
quantityUnit: resolvedEnteredUnit,
|
|
1471
|
+
normalizedQuantity,
|
|
1472
|
+
normalizedUnit: baseUnitCode,
|
|
1473
|
+
uomSnapshot: snapshot
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
978
1476
|
function normalizeShippingMethodContext(snapshot, id, code, currencyCode) {
|
|
979
1477
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
980
1478
|
const metadata = snapshot.metadata;
|
|
@@ -1052,6 +1550,9 @@ function mapOrderLineEntityToSnapshot(line) {
|
|
|
1052
1550
|
comment: line.comment ?? null,
|
|
1053
1551
|
quantity: toNumeric(line.quantity),
|
|
1054
1552
|
quantityUnit: line.quantityUnit ?? null,
|
|
1553
|
+
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
1554
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
1555
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
1055
1556
|
currencyCode: line.currencyCode,
|
|
1056
1557
|
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
1057
1558
|
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
@@ -1079,6 +1580,9 @@ function mapQuoteLineEntityToSnapshot(line) {
|
|
|
1079
1580
|
comment: line.comment ?? null,
|
|
1080
1581
|
quantity: toNumeric(line.quantity),
|
|
1081
1582
|
quantityUnit: line.quantityUnit ?? null,
|
|
1583
|
+
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
1584
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
1585
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
1082
1586
|
currencyCode: line.currencyCode,
|
|
1083
1587
|
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
1084
1588
|
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
@@ -1133,6 +1637,10 @@ async function emitTotalsCalculated(eventBus, payload) {
|
|
|
1133
1637
|
await eventBus.emitEvent("sales.document.totals.calculated", payload);
|
|
1134
1638
|
}
|
|
1135
1639
|
function createLineSnapshotFromInput(line, lineNumber) {
|
|
1640
|
+
const quantity = Number(line.quantity ?? 0);
|
|
1641
|
+
const normalizedQuantity = toNumeric(
|
|
1642
|
+
line.normalizedQuantity ?? quantity
|
|
1643
|
+
);
|
|
1136
1644
|
return {
|
|
1137
1645
|
lineNumber,
|
|
1138
1646
|
kind: line.kind ?? "product",
|
|
@@ -1141,8 +1649,11 @@ function createLineSnapshotFromInput(line, lineNumber) {
|
|
|
1141
1649
|
name: line.name ?? null,
|
|
1142
1650
|
description: line.description ?? null,
|
|
1143
1651
|
comment: line.comment ?? null,
|
|
1144
|
-
quantity
|
|
1652
|
+
quantity,
|
|
1145
1653
|
quantityUnit: line.quantityUnit ?? null,
|
|
1654
|
+
normalizedQuantity,
|
|
1655
|
+
normalizedUnit: typeof line.normalizedUnit === "string" ? line.normalizedUnit : line.quantityUnit ?? null,
|
|
1656
|
+
uomSnapshot: line.uomSnapshot && typeof line.uomSnapshot === "object" ? cloneJson(line.uomSnapshot) : null,
|
|
1146
1657
|
currencyCode: line.currencyCode,
|
|
1147
1658
|
unitPriceNet: line.unitPriceNet ?? null,
|
|
1148
1659
|
unitPriceGross: line.unitPriceGross ?? null,
|
|
@@ -1162,7 +1673,9 @@ function createLineSnapshotFromInput(line, lineNumber) {
|
|
|
1162
1673
|
function createAdjustmentDraftFromInput(adjustment) {
|
|
1163
1674
|
const lineRef = "quoteLineId" in adjustment ? adjustment.quoteLineId : adjustment.orderLineId;
|
|
1164
1675
|
if (adjustment.scope === "line" && lineRef) {
|
|
1165
|
-
throw new CrudHttpError(400, {
|
|
1676
|
+
throw new CrudHttpError(400, {
|
|
1677
|
+
error: "Line-scoped adjustments are not supported yet."
|
|
1678
|
+
});
|
|
1166
1679
|
}
|
|
1167
1680
|
return {
|
|
1168
1681
|
id: typeof adjustment.id === "string" ? adjustment.id : void 0,
|
|
@@ -1194,8 +1707,13 @@ function convertLineCalculationToEntityInput(lineResult, sourceLine, document, i
|
|
|
1194
1707
|
comment: line.comment ?? null,
|
|
1195
1708
|
quantity: toNumericString(line.quantity) ?? "0",
|
|
1196
1709
|
quantityUnit: line.quantityUnit ?? null,
|
|
1710
|
+
normalizedQuantity: toNumericString(line.normalizedQuantity ?? line.quantity) ?? "0",
|
|
1711
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
1712
|
+
uomSnapshot: line.uomSnapshot && typeof line.uomSnapshot === "object" ? cloneJson(line.uomSnapshot) : null,
|
|
1197
1713
|
currencyCode: line.currencyCode,
|
|
1198
|
-
unitPriceNet: toNumericString(
|
|
1714
|
+
unitPriceNet: toNumericString(
|
|
1715
|
+
line.unitPriceNet ?? lineResult.netAmount / Math.max(line.quantity || 1, 1)
|
|
1716
|
+
) ?? "0",
|
|
1199
1717
|
unitPriceGross: toNumericString(
|
|
1200
1718
|
line.unitPriceGross ?? lineResult.grossAmount / Math.max(line.quantity || 1, 1)
|
|
1201
1719
|
) ?? "0",
|
|
@@ -1251,7 +1769,12 @@ async function applyOrderLineResults(params) {
|
|
|
1251
1769
|
const sourceLine = sourceLines[index];
|
|
1252
1770
|
const statusEntryId = sourceLine.statusEntryId ?? null;
|
|
1253
1771
|
const statusValue = await resolveStatus(statusEntryId ?? null);
|
|
1254
|
-
const payload = convertLineCalculationToEntityInput(
|
|
1772
|
+
const payload = convertLineCalculationToEntityInput(
|
|
1773
|
+
lineResult,
|
|
1774
|
+
sourceLine,
|
|
1775
|
+
order,
|
|
1776
|
+
index
|
|
1777
|
+
);
|
|
1255
1778
|
const existing = sourceLine.id ? existingMap.get(sourceLine.id) ?? null : null;
|
|
1256
1779
|
const lineEntity = existing ?? em.create(SalesOrderLine, {
|
|
1257
1780
|
order,
|
|
@@ -1306,7 +1829,12 @@ async function applyQuoteLineResults(params) {
|
|
|
1306
1829
|
const sourceLine = sourceLines[index];
|
|
1307
1830
|
const statusEntryId = sourceLine.statusEntryId ?? null;
|
|
1308
1831
|
const statusValue = await resolveStatus(statusEntryId ?? null);
|
|
1309
|
-
const payload = convertLineCalculationToEntityInput(
|
|
1832
|
+
const payload = convertLineCalculationToEntityInput(
|
|
1833
|
+
lineResult,
|
|
1834
|
+
sourceLine,
|
|
1835
|
+
quote,
|
|
1836
|
+
index
|
|
1837
|
+
);
|
|
1310
1838
|
const existing = sourceLine.id ? existingMap.get(sourceLine.id) ?? null : null;
|
|
1311
1839
|
const lineEntity = existing ?? em.create(SalesQuoteLine, {
|
|
1312
1840
|
quote,
|
|
@@ -1353,7 +1881,12 @@ async function replaceQuoteLines(em, quote, calculation, lineInputs) {
|
|
|
1353
1881
|
for (let index = 0; index < calculation.lines.length; index += 1) {
|
|
1354
1882
|
const lineResult = calculation.lines[index];
|
|
1355
1883
|
const sourceLine = lineInputs[index];
|
|
1356
|
-
const entityInput = convertLineCalculationToEntityInput(
|
|
1884
|
+
const entityInput = convertLineCalculationToEntityInput(
|
|
1885
|
+
lineResult,
|
|
1886
|
+
sourceLine,
|
|
1887
|
+
quote,
|
|
1888
|
+
index
|
|
1889
|
+
);
|
|
1357
1890
|
const statusValue = await resolveStatus(sourceLine.statusEntryId ?? null);
|
|
1358
1891
|
const lineEntity = em.create(SalesQuoteLine, {
|
|
1359
1892
|
quote,
|
|
@@ -1377,7 +1910,11 @@ async function replaceQuoteLines(em, quote, calculation, lineInputs) {
|
|
|
1377
1910
|
}
|
|
1378
1911
|
}
|
|
1379
1912
|
async function replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs) {
|
|
1380
|
-
const existing = await em.find(
|
|
1913
|
+
const existing = await em.find(
|
|
1914
|
+
SalesQuoteAdjustment,
|
|
1915
|
+
{ quote },
|
|
1916
|
+
{ orderBy: { position: "asc" } }
|
|
1917
|
+
);
|
|
1381
1918
|
const existingMap = /* @__PURE__ */ new Map();
|
|
1382
1919
|
existing.forEach((adj) => existingMap.set(adj.id, adj));
|
|
1383
1920
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1386,7 +1923,12 @@ async function replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs)
|
|
|
1386
1923
|
const draft = adjustmentDrafts[index];
|
|
1387
1924
|
const sourceById = adjustmentInputs?.find((adj) => adj.id === draft.id) ?? null;
|
|
1388
1925
|
const source = sourceById ?? (adjustmentInputs ? adjustmentInputs[index] ?? null : null);
|
|
1389
|
-
const entityInput = convertAdjustmentResultToEntityInput(
|
|
1926
|
+
const entityInput = convertAdjustmentResultToEntityInput(
|
|
1927
|
+
draft,
|
|
1928
|
+
source,
|
|
1929
|
+
quote,
|
|
1930
|
+
index
|
|
1931
|
+
);
|
|
1390
1932
|
const adjustmentId = draft.id ?? source?.id ?? randomUUID();
|
|
1391
1933
|
const existingEntity = existingMap.get(adjustmentId);
|
|
1392
1934
|
const entity = existingEntity ?? em.create(SalesQuoteAdjustment, {
|
|
@@ -1441,7 +1983,12 @@ async function replaceOrderLines(em, order, calculation, lineInputs) {
|
|
|
1441
1983
|
for (let index = 0; index < calculation.lines.length; index += 1) {
|
|
1442
1984
|
const lineResult = calculation.lines[index];
|
|
1443
1985
|
const sourceLine = lineInputs[index];
|
|
1444
|
-
const entityInput = convertLineCalculationToEntityInput(
|
|
1986
|
+
const entityInput = convertLineCalculationToEntityInput(
|
|
1987
|
+
lineResult,
|
|
1988
|
+
sourceLine,
|
|
1989
|
+
order,
|
|
1990
|
+
index
|
|
1991
|
+
);
|
|
1445
1992
|
const statusValue = await resolveStatus(sourceLine.statusEntryId ?? null);
|
|
1446
1993
|
const lineEntity = em.create(SalesOrderLine, {
|
|
1447
1994
|
order,
|
|
@@ -1469,7 +2016,11 @@ async function replaceOrderLines(em, order, calculation, lineInputs) {
|
|
|
1469
2016
|
}
|
|
1470
2017
|
}
|
|
1471
2018
|
async function replaceOrderAdjustments(em, order, calculation, adjustmentInputs) {
|
|
1472
|
-
const existing = await em.find(
|
|
2019
|
+
const existing = await em.find(
|
|
2020
|
+
SalesOrderAdjustment,
|
|
2021
|
+
{ order },
|
|
2022
|
+
{ orderBy: { position: "asc" } }
|
|
2023
|
+
);
|
|
1473
2024
|
const existingMap = /* @__PURE__ */ new Map();
|
|
1474
2025
|
existing.forEach((adj) => existingMap.set(adj.id, adj));
|
|
1475
2026
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1478,7 +2029,12 @@ async function replaceOrderAdjustments(em, order, calculation, adjustmentInputs)
|
|
|
1478
2029
|
const draft = adjustmentDrafts[index];
|
|
1479
2030
|
const sourceById = adjustmentInputs?.find((adj) => adj.id === draft.id) ?? null;
|
|
1480
2031
|
const source = sourceById ?? (adjustmentInputs ? adjustmentInputs[index] ?? null : null);
|
|
1481
|
-
const entityInput = convertAdjustmentResultToEntityInput(
|
|
2032
|
+
const entityInput = convertAdjustmentResultToEntityInput(
|
|
2033
|
+
draft,
|
|
2034
|
+
source,
|
|
2035
|
+
order,
|
|
2036
|
+
index
|
|
2037
|
+
);
|
|
1482
2038
|
const adjustmentId = draft.id ?? source?.id ?? randomUUID();
|
|
1483
2039
|
const existingEntity = existingMap.get(adjustmentId);
|
|
1484
2040
|
const entity = existingEntity ?? em.create(SalesOrderAdjustment, {
|
|
@@ -1547,7 +2103,8 @@ function applyOrderTotals(order, totals, lineCount) {
|
|
|
1547
2103
|
order.lineItemCount = lineCount;
|
|
1548
2104
|
}
|
|
1549
2105
|
function normalizePaymentTotal(value) {
|
|
1550
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
2106
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2107
|
+
return Math.max(value, 0);
|
|
1551
2108
|
if (typeof value === "string" && value.trim().length) {
|
|
1552
2109
|
const parsed = Number(value);
|
|
1553
2110
|
return Number.isFinite(parsed) ? Math.max(parsed, 0) : 0;
|
|
@@ -1645,7 +2202,10 @@ async function syncSalesDocumentTags(em, params) {
|
|
|
1645
2202
|
if (params.tagIds === void 0) return;
|
|
1646
2203
|
const tagIds = normalizeTagIds(params.tagIds);
|
|
1647
2204
|
if (tagIds.length === 0) {
|
|
1648
|
-
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2205
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2206
|
+
documentId: params.documentId,
|
|
2207
|
+
documentKind: params.kind
|
|
2208
|
+
});
|
|
1649
2209
|
return;
|
|
1650
2210
|
}
|
|
1651
2211
|
const tagsInScope = await em.find(SalesDocumentTag, {
|
|
@@ -1654,10 +2214,15 @@ async function syncSalesDocumentTags(em, params) {
|
|
|
1654
2214
|
tenantId: params.tenantId
|
|
1655
2215
|
});
|
|
1656
2216
|
if (tagsInScope.length !== tagIds.length) {
|
|
1657
|
-
throw new CrudHttpError(400, {
|
|
2217
|
+
throw new CrudHttpError(400, {
|
|
2218
|
+
error: "One or more tags not found for this scope"
|
|
2219
|
+
});
|
|
1658
2220
|
}
|
|
1659
2221
|
const byId = new Map(tagsInScope.map((tag) => [tag.id, tag]));
|
|
1660
|
-
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2222
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2223
|
+
documentId: params.documentId,
|
|
2224
|
+
documentKind: params.kind
|
|
2225
|
+
});
|
|
1661
2226
|
for (const tagId of tagIds) {
|
|
1662
2227
|
const tag = byId.get(tagId);
|
|
1663
2228
|
if (!tag) continue;
|
|
@@ -1815,9 +2380,20 @@ async function restoreQuoteGraph(em, snapshot) {
|
|
|
1815
2380
|
}
|
|
1816
2381
|
applyQuoteSnapshot(quote, snapshot.quote);
|
|
1817
2382
|
await em.flush();
|
|
1818
|
-
const existingLines = await em.find(
|
|
1819
|
-
|
|
1820
|
-
|
|
2383
|
+
const existingLines = await em.find(
|
|
2384
|
+
SalesQuoteLine,
|
|
2385
|
+
{ quote: quote.id },
|
|
2386
|
+
{ fields: ["id"] }
|
|
2387
|
+
);
|
|
2388
|
+
const existingAdjustments = await em.find(
|
|
2389
|
+
SalesQuoteAdjustment,
|
|
2390
|
+
{ quote: quote.id },
|
|
2391
|
+
{ fields: ["id"] }
|
|
2392
|
+
);
|
|
2393
|
+
await em.nativeDelete(CustomFieldValue, {
|
|
2394
|
+
entityId: E.sales.sales_quote,
|
|
2395
|
+
recordId: quote.id
|
|
2396
|
+
});
|
|
1821
2397
|
if (existingLines.length) {
|
|
1822
2398
|
await em.nativeDelete(CustomFieldValue, {
|
|
1823
2399
|
entityId: E.sales.sales_quote_line,
|
|
@@ -1833,13 +2409,24 @@ async function restoreQuoteGraph(em, snapshot) {
|
|
|
1833
2409
|
const addressSnapshots = Array.isArray(snapshot.addresses) ? snapshot.addresses : [];
|
|
1834
2410
|
const noteSnapshots = Array.isArray(snapshot.notes) ? snapshot.notes : [];
|
|
1835
2411
|
const tagSnapshots = Array.isArray(snapshot.tags) ? snapshot.tags : [];
|
|
1836
|
-
await em.nativeDelete(SalesDocumentAddress, {
|
|
1837
|
-
|
|
1838
|
-
|
|
2412
|
+
await em.nativeDelete(SalesDocumentAddress, {
|
|
2413
|
+
documentId: quote.id,
|
|
2414
|
+
documentKind: "quote"
|
|
2415
|
+
});
|
|
2416
|
+
await em.nativeDelete(SalesNote, {
|
|
2417
|
+
contextType: "quote",
|
|
2418
|
+
contextId: quote.id
|
|
2419
|
+
});
|
|
2420
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2421
|
+
documentId: quote.id,
|
|
2422
|
+
documentKind: "quote"
|
|
2423
|
+
});
|
|
1839
2424
|
await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
|
|
1840
2425
|
await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
|
|
1841
2426
|
existingLines.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
|
|
1842
|
-
existingAdjustments.forEach(
|
|
2427
|
+
existingAdjustments.forEach(
|
|
2428
|
+
(entry) => em.getUnitOfWork().unsetIdentity(entry)
|
|
2429
|
+
);
|
|
1843
2430
|
snapshot.lines.forEach((line) => {
|
|
1844
2431
|
const lineEntity = em.create(SalesQuoteLine, {
|
|
1845
2432
|
id: line.id,
|
|
@@ -1858,6 +2445,9 @@ async function restoreQuoteGraph(em, snapshot) {
|
|
|
1858
2445
|
comment: line.comment ?? null,
|
|
1859
2446
|
quantity: line.quantity,
|
|
1860
2447
|
quantityUnit: line.quantityUnit ?? null,
|
|
2448
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
2449
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
2450
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
1861
2451
|
currencyCode: line.currencyCode,
|
|
1862
2452
|
unitPriceNet: line.unitPriceNet,
|
|
1863
2453
|
unitPriceGross: line.unitPriceGross,
|
|
@@ -2056,9 +2646,20 @@ async function restoreOrderGraph(em, snapshot) {
|
|
|
2056
2646
|
}
|
|
2057
2647
|
applyOrderSnapshot(order, snapshot.order);
|
|
2058
2648
|
await em.flush();
|
|
2059
|
-
const existingLines = await em.find(
|
|
2060
|
-
|
|
2061
|
-
|
|
2649
|
+
const existingLines = await em.find(
|
|
2650
|
+
SalesOrderLine,
|
|
2651
|
+
{ order: order.id },
|
|
2652
|
+
{ fields: ["id"] }
|
|
2653
|
+
);
|
|
2654
|
+
const existingAdjustments = await em.find(
|
|
2655
|
+
SalesOrderAdjustment,
|
|
2656
|
+
{ order: order.id },
|
|
2657
|
+
{ fields: ["id"] }
|
|
2658
|
+
);
|
|
2659
|
+
await em.nativeDelete(CustomFieldValue, {
|
|
2660
|
+
entityId: E.sales.sales_order,
|
|
2661
|
+
recordId: order.id
|
|
2662
|
+
});
|
|
2062
2663
|
if (existingLines.length) {
|
|
2063
2664
|
await em.nativeDelete(CustomFieldValue, {
|
|
2064
2665
|
entityId: E.sales.sales_order_line,
|
|
@@ -2079,19 +2680,34 @@ async function restoreOrderGraph(em, snapshot) {
|
|
|
2079
2680
|
const existingShipments = await em.find(SalesShipment, { order: order.id });
|
|
2080
2681
|
const shipmentIds = existingShipments.map((entry) => entry.id);
|
|
2081
2682
|
if (shipmentIds.length) {
|
|
2082
|
-
await em.nativeDelete(SalesShipmentItem, {
|
|
2683
|
+
await em.nativeDelete(SalesShipmentItem, {
|
|
2684
|
+
shipment: { $in: shipmentIds }
|
|
2685
|
+
});
|
|
2083
2686
|
await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
|
|
2084
|
-
existingShipments.forEach(
|
|
2687
|
+
existingShipments.forEach(
|
|
2688
|
+
(entry) => em.getUnitOfWork().unsetIdentity(entry)
|
|
2689
|
+
);
|
|
2085
2690
|
}
|
|
2086
2691
|
await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
|
|
2087
2692
|
await em.nativeDelete(SalesPayment, { order: order.id });
|
|
2088
|
-
await em.nativeDelete(SalesDocumentAddress, {
|
|
2089
|
-
|
|
2090
|
-
|
|
2693
|
+
await em.nativeDelete(SalesDocumentAddress, {
|
|
2694
|
+
documentId: order.id,
|
|
2695
|
+
documentKind: "order"
|
|
2696
|
+
});
|
|
2697
|
+
await em.nativeDelete(SalesNote, {
|
|
2698
|
+
contextType: "order",
|
|
2699
|
+
contextId: order.id
|
|
2700
|
+
});
|
|
2701
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
2702
|
+
documentId: order.id,
|
|
2703
|
+
documentKind: "order"
|
|
2704
|
+
});
|
|
2091
2705
|
await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
|
|
2092
2706
|
await em.nativeDelete(SalesOrderLine, { order: order.id });
|
|
2093
2707
|
existingLines.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
|
|
2094
|
-
existingAdjustments.forEach(
|
|
2708
|
+
existingAdjustments.forEach(
|
|
2709
|
+
(entry) => em.getUnitOfWork().unsetIdentity(entry)
|
|
2710
|
+
);
|
|
2095
2711
|
snapshot.lines.forEach((line) => {
|
|
2096
2712
|
const lineEntity = em.create(SalesOrderLine, {
|
|
2097
2713
|
id: line.id,
|
|
@@ -2110,6 +2726,9 @@ async function restoreOrderGraph(em, snapshot) {
|
|
|
2110
2726
|
comment: line.comment ?? null,
|
|
2111
2727
|
quantity: line.quantity,
|
|
2112
2728
|
quantityUnit: line.quantityUnit ?? null,
|
|
2729
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
2730
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
2731
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
2113
2732
|
reservedQuantity: line.reservedQuantity,
|
|
2114
2733
|
fulfilledQuantity: line.fulfilledQuantity,
|
|
2115
2734
|
invoicedQuantity: line.invoicedQuantity,
|
|
@@ -2257,7 +2876,9 @@ async function restoreOrderGraph(em, snapshot) {
|
|
|
2257
2876
|
const createQuoteCommand = {
|
|
2258
2877
|
id: "sales.quotes.create",
|
|
2259
2878
|
async execute(rawInput, ctx) {
|
|
2260
|
-
const generator = ctx.container.resolve(
|
|
2879
|
+
const generator = ctx.container.resolve(
|
|
2880
|
+
"salesDocumentNumberGenerator"
|
|
2881
|
+
);
|
|
2261
2882
|
const initial = quoteCreateSchema.parse(rawInput ?? {});
|
|
2262
2883
|
const quoteNumber = typeof initial.quoteNumber === "string" && initial.quoteNumber.trim().length ? initial.quoteNumber.trim() : (await generator.generate({
|
|
2263
2884
|
kind: "quote",
|
|
@@ -2279,7 +2900,10 @@ const createQuoteCommand = {
|
|
|
2279
2900
|
deliveryWindow,
|
|
2280
2901
|
paymentMethod
|
|
2281
2902
|
} = await resolveDocumentReferences(em, parsed);
|
|
2282
|
-
const quoteStatus = await resolveDictionaryEntryValue(
|
|
2903
|
+
const quoteStatus = await resolveDictionaryEntryValue(
|
|
2904
|
+
em,
|
|
2905
|
+
parsed.statusEntryId ?? null
|
|
2906
|
+
);
|
|
2283
2907
|
const quoteId = randomUUID();
|
|
2284
2908
|
const quote = em.create(SalesQuote, {
|
|
2285
2909
|
id: quoteId,
|
|
@@ -2370,6 +2994,26 @@ const createQuoteCommand = {
|
|
|
2370
2994
|
lineNumber: line.lineNumber ?? index + 1
|
|
2371
2995
|
})
|
|
2372
2996
|
);
|
|
2997
|
+
const uomResolver = createUomResolver();
|
|
2998
|
+
const normalizedLineInputs = await Promise.all(
|
|
2999
|
+
lineInputs.map(async (line) => {
|
|
3000
|
+
const normalized = await normalizeLineUom({
|
|
3001
|
+
em,
|
|
3002
|
+
resolver: uomResolver,
|
|
3003
|
+
organizationId: parsed.organizationId,
|
|
3004
|
+
tenantId: parsed.tenantId,
|
|
3005
|
+
line
|
|
3006
|
+
});
|
|
3007
|
+
return {
|
|
3008
|
+
...line,
|
|
3009
|
+
quantity: normalized.quantity,
|
|
3010
|
+
quantityUnit: normalized.quantityUnit,
|
|
3011
|
+
normalizedQuantity: normalized.normalizedQuantity,
|
|
3012
|
+
normalizedUnit: normalized.normalizedUnit,
|
|
3013
|
+
uomSnapshot: normalized.uomSnapshot
|
|
3014
|
+
};
|
|
3015
|
+
})
|
|
3016
|
+
);
|
|
2373
3017
|
const adjustmentInputs = parsed.adjustments ? parsed.adjustments.map(
|
|
2374
3018
|
(adj) => quoteAdjustmentCreateSchema.parse({
|
|
2375
3019
|
...adj,
|
|
@@ -2378,7 +3022,7 @@ const createQuoteCommand = {
|
|
|
2378
3022
|
quoteId: quote.id
|
|
2379
3023
|
})
|
|
2380
3024
|
) : null;
|
|
2381
|
-
const lineSnapshots =
|
|
3025
|
+
const lineSnapshots = normalizedLineInputs.map(
|
|
2382
3026
|
(line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
|
|
2383
3027
|
);
|
|
2384
3028
|
const adjustmentDrafts = adjustmentInputs ? adjustmentInputs.map((adj) => createAdjustmentDraftFromInput(adj)) : [];
|
|
@@ -2400,7 +3044,7 @@ const createQuoteCommand = {
|
|
|
2400
3044
|
adjustments: adjustmentDrafts,
|
|
2401
3045
|
context: calculationContext
|
|
2402
3046
|
});
|
|
2403
|
-
await replaceQuoteLines(em, quote, calculation,
|
|
3047
|
+
await replaceQuoteLines(em, quote, calculation, normalizedLineInputs);
|
|
2404
3048
|
await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
|
|
2405
3049
|
applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
|
|
2406
3050
|
let eventBus = null;
|
|
@@ -2428,7 +3072,9 @@ const createQuoteCommand = {
|
|
|
2428
3072
|
await em.flush();
|
|
2429
3073
|
try {
|
|
2430
3074
|
const notificationService = resolveNotificationService(ctx.container);
|
|
2431
|
-
const typeDef = notificationTypes.find(
|
|
3075
|
+
const typeDef = notificationTypes.find(
|
|
3076
|
+
(type) => type.type === "sales.quote.created"
|
|
3077
|
+
);
|
|
2432
3078
|
if (typeDef) {
|
|
2433
3079
|
const totalAmount = quote.grandTotalGrossAmount && quote.currencyCode ? `${quote.grandTotalGrossAmount} ${quote.currencyCode}` : "";
|
|
2434
3080
|
const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
|
|
@@ -2449,7 +3095,10 @@ const createQuoteCommand = {
|
|
|
2449
3095
|
});
|
|
2450
3096
|
}
|
|
2451
3097
|
} catch (err) {
|
|
2452
|
-
console.error(
|
|
3098
|
+
console.error(
|
|
3099
|
+
"[sales.quotes.create] Failed to create notification:",
|
|
3100
|
+
err
|
|
3101
|
+
);
|
|
2453
3102
|
}
|
|
2454
3103
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
2455
3104
|
await emitCrudSideEffects({
|
|
@@ -2468,7 +3117,11 @@ const createQuoteCommand = {
|
|
|
2468
3117
|
await invalidateCrudCache(
|
|
2469
3118
|
ctx.container,
|
|
2470
3119
|
resourceKind,
|
|
2471
|
-
{
|
|
3120
|
+
{
|
|
3121
|
+
id: quote.id,
|
|
3122
|
+
organizationId: quote.organizationId,
|
|
3123
|
+
tenantId: quote.tenantId
|
|
3124
|
+
},
|
|
2472
3125
|
ctx.auth?.tenantId ?? null,
|
|
2473
3126
|
"created"
|
|
2474
3127
|
);
|
|
@@ -2517,7 +3170,11 @@ const deleteQuoteCommand = {
|
|
|
2517
3170
|
const em = ctx.container.resolve("em");
|
|
2518
3171
|
const snapshot = await loadQuoteSnapshot(em, id);
|
|
2519
3172
|
if (snapshot) {
|
|
2520
|
-
ensureQuoteScope(
|
|
3173
|
+
ensureQuoteScope(
|
|
3174
|
+
ctx,
|
|
3175
|
+
snapshot.quote.organizationId,
|
|
3176
|
+
snapshot.quote.tenantId
|
|
3177
|
+
);
|
|
2521
3178
|
}
|
|
2522
3179
|
return snapshot ? { before: snapshot } : {};
|
|
2523
3180
|
},
|
|
@@ -2525,18 +3182,34 @@ const deleteQuoteCommand = {
|
|
|
2525
3182
|
const id = requireId(input, "Quote id is required");
|
|
2526
3183
|
const em = ctx.container.resolve("em").fork();
|
|
2527
3184
|
const quote = await em.findOne(SalesQuote, { id });
|
|
2528
|
-
if (!quote)
|
|
3185
|
+
if (!quote)
|
|
3186
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
2529
3187
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
2530
3188
|
const [addresses, notes, tags, adjustments, lines] = await Promise.all([
|
|
2531
|
-
em.find(SalesDocumentAddress, {
|
|
3189
|
+
em.find(SalesDocumentAddress, {
|
|
3190
|
+
documentId: quote.id,
|
|
3191
|
+
documentKind: "quote"
|
|
3192
|
+
}),
|
|
2532
3193
|
em.find(SalesNote, { contextType: "quote", contextId: quote.id }),
|
|
2533
|
-
em.find(SalesDocumentTagAssignment, {
|
|
3194
|
+
em.find(SalesDocumentTagAssignment, {
|
|
3195
|
+
documentId: quote.id,
|
|
3196
|
+
documentKind: "quote"
|
|
3197
|
+
}),
|
|
2534
3198
|
em.find(SalesQuoteAdjustment, { quote: quote.id }),
|
|
2535
3199
|
em.find(SalesQuoteLine, { quote: quote.id })
|
|
2536
3200
|
]);
|
|
2537
|
-
await em.nativeDelete(SalesDocumentAddress, {
|
|
2538
|
-
|
|
2539
|
-
|
|
3201
|
+
await em.nativeDelete(SalesDocumentAddress, {
|
|
3202
|
+
documentId: quote.id,
|
|
3203
|
+
documentKind: "quote"
|
|
3204
|
+
});
|
|
3205
|
+
await em.nativeDelete(SalesNote, {
|
|
3206
|
+
contextType: "quote",
|
|
3207
|
+
contextId: quote.id
|
|
3208
|
+
});
|
|
3209
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
3210
|
+
documentId: quote.id,
|
|
3211
|
+
documentKind: "quote"
|
|
3212
|
+
});
|
|
2540
3213
|
await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
|
|
2541
3214
|
await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
|
|
2542
3215
|
em.remove(quote);
|
|
@@ -2545,10 +3218,22 @@ const deleteQuoteCommand = {
|
|
|
2545
3218
|
await Promise.all([
|
|
2546
3219
|
queueDeletionSideEffects(dataEngine, quote, E.sales.sales_quote),
|
|
2547
3220
|
queueDeletionSideEffects(dataEngine, lines, E.sales.sales_quote_line),
|
|
2548
|
-
queueDeletionSideEffects(
|
|
2549
|
-
|
|
3221
|
+
queueDeletionSideEffects(
|
|
3222
|
+
dataEngine,
|
|
3223
|
+
adjustments,
|
|
3224
|
+
E.sales.sales_quote_adjustment
|
|
3225
|
+
),
|
|
3226
|
+
queueDeletionSideEffects(
|
|
3227
|
+
dataEngine,
|
|
3228
|
+
addresses,
|
|
3229
|
+
E.sales.sales_document_address
|
|
3230
|
+
),
|
|
2550
3231
|
queueDeletionSideEffects(dataEngine, notes, E.sales.sales_note),
|
|
2551
|
-
queueDeletionSideEffects(
|
|
3232
|
+
queueDeletionSideEffects(
|
|
3233
|
+
dataEngine,
|
|
3234
|
+
tags,
|
|
3235
|
+
E.sales.sales_document_tag_assignment
|
|
3236
|
+
)
|
|
2552
3237
|
]);
|
|
2553
3238
|
return { quoteId: id };
|
|
2554
3239
|
},
|
|
@@ -2587,15 +3272,23 @@ const updateQuoteCommand = {
|
|
|
2587
3272
|
const em = ctx.container.resolve("em");
|
|
2588
3273
|
const snapshot = await loadQuoteSnapshot(em, parsed.id);
|
|
2589
3274
|
if (snapshot) {
|
|
2590
|
-
ensureQuoteScope(
|
|
3275
|
+
ensureQuoteScope(
|
|
3276
|
+
ctx,
|
|
3277
|
+
snapshot.quote.organizationId,
|
|
3278
|
+
snapshot.quote.tenantId
|
|
3279
|
+
);
|
|
2591
3280
|
}
|
|
2592
3281
|
return snapshot ? { before: snapshot } : {};
|
|
2593
3282
|
},
|
|
2594
3283
|
async execute(rawInput, ctx) {
|
|
2595
3284
|
const parsed = documentUpdateSchema.parse(rawInput ?? {});
|
|
2596
3285
|
const em = ctx.container.resolve("em").fork();
|
|
2597
|
-
const quote = await em.findOne(SalesQuote, {
|
|
2598
|
-
|
|
3286
|
+
const quote = await em.findOne(SalesQuote, {
|
|
3287
|
+
id: parsed.id,
|
|
3288
|
+
deletedAt: null
|
|
3289
|
+
});
|
|
3290
|
+
if (!quote)
|
|
3291
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
2599
3292
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
2600
3293
|
const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
|
|
2601
3294
|
if (shouldInvalidateSentToken) {
|
|
@@ -2603,7 +3296,12 @@ const updateQuoteCommand = {
|
|
|
2603
3296
|
quote.sentAt = null;
|
|
2604
3297
|
}
|
|
2605
3298
|
const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0;
|
|
2606
|
-
await applyDocumentUpdate({
|
|
3299
|
+
await applyDocumentUpdate({
|
|
3300
|
+
kind: "quote",
|
|
3301
|
+
entity: quote,
|
|
3302
|
+
input: parsed,
|
|
3303
|
+
em
|
|
3304
|
+
});
|
|
2607
3305
|
await em.flush();
|
|
2608
3306
|
if (shouldInvalidateSentToken) {
|
|
2609
3307
|
quote.status = "draft";
|
|
@@ -2616,7 +3314,11 @@ const updateQuoteCommand = {
|
|
|
2616
3314
|
if (shouldRecalculateTotals) {
|
|
2617
3315
|
const [existingLines, adjustments] = await Promise.all([
|
|
2618
3316
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
2619
|
-
em.find(
|
|
3317
|
+
em.find(
|
|
3318
|
+
SalesQuoteAdjustment,
|
|
3319
|
+
{ quote },
|
|
3320
|
+
{ orderBy: { position: "asc" } }
|
|
3321
|
+
)
|
|
2620
3322
|
]);
|
|
2621
3323
|
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
2622
3324
|
const calcLines = lineSnapshots.map(
|
|
@@ -2635,7 +3337,9 @@ const updateQuoteCommand = {
|
|
|
2635
3337
|
)
|
|
2636
3338
|
);
|
|
2637
3339
|
const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
|
|
2638
|
-
const salesCalculationService = ctx.container.resolve(
|
|
3340
|
+
const salesCalculationService = ctx.container.resolve(
|
|
3341
|
+
"salesCalculationService"
|
|
3342
|
+
);
|
|
2639
3343
|
const calculationContext = buildCalculationContext({
|
|
2640
3344
|
tenantId: quote.tenantId,
|
|
2641
3345
|
organizationId: quote.organizationId,
|
|
@@ -2647,12 +3351,14 @@ const updateQuoteCommand = {
|
|
|
2647
3351
|
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
2648
3352
|
paymentMethodCode: quote.paymentMethodCode ?? null
|
|
2649
3353
|
});
|
|
2650
|
-
const calculation = await salesCalculationService.calculateDocumentTotals(
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3354
|
+
const calculation = await salesCalculationService.calculateDocumentTotals(
|
|
3355
|
+
{
|
|
3356
|
+
documentKind: "quote",
|
|
3357
|
+
lines: calcLines,
|
|
3358
|
+
adjustments: adjustmentDrafts,
|
|
3359
|
+
context: calculationContext
|
|
3360
|
+
}
|
|
3361
|
+
);
|
|
2656
3362
|
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
2657
3363
|
organizationId: quote.organizationId,
|
|
2658
3364
|
tenantId: quote.tenantId,
|
|
@@ -2694,7 +3400,11 @@ const updateQuoteCommand = {
|
|
|
2694
3400
|
await invalidateCrudCache(
|
|
2695
3401
|
ctx.container,
|
|
2696
3402
|
resourceKind,
|
|
2697
|
-
{
|
|
3403
|
+
{
|
|
3404
|
+
id: quote.id,
|
|
3405
|
+
organizationId: quote.organizationId,
|
|
3406
|
+
tenantId: quote.tenantId
|
|
3407
|
+
},
|
|
2698
3408
|
ctx.auth?.tenantId ?? null,
|
|
2699
3409
|
"updated"
|
|
2700
3410
|
);
|
|
@@ -2753,25 +3463,42 @@ const updateOrderCommand = {
|
|
|
2753
3463
|
const em = ctx.container.resolve("em");
|
|
2754
3464
|
const snapshot = await loadOrderSnapshot(em, parsed.id);
|
|
2755
3465
|
if (snapshot) {
|
|
2756
|
-
ensureOrderScope(
|
|
3466
|
+
ensureOrderScope(
|
|
3467
|
+
ctx,
|
|
3468
|
+
snapshot.order.organizationId,
|
|
3469
|
+
snapshot.order.tenantId
|
|
3470
|
+
);
|
|
2757
3471
|
}
|
|
2758
3472
|
return snapshot ? { before: snapshot } : {};
|
|
2759
3473
|
},
|
|
2760
3474
|
async execute(rawInput, ctx) {
|
|
2761
3475
|
const parsed = documentUpdateSchema.parse(rawInput ?? {});
|
|
2762
3476
|
const em = ctx.container.resolve("em").fork();
|
|
2763
|
-
const order = await em.findOne(SalesOrder, {
|
|
2764
|
-
|
|
3477
|
+
const order = await em.findOne(SalesOrder, {
|
|
3478
|
+
id: parsed.id,
|
|
3479
|
+
deletedAt: null
|
|
3480
|
+
});
|
|
3481
|
+
if (!order)
|
|
3482
|
+
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
2765
3483
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
2766
3484
|
const previousStatus = normalizeStatusValue(order.status);
|
|
2767
3485
|
let statusChangeNote = null;
|
|
2768
3486
|
const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0;
|
|
2769
|
-
await applyDocumentUpdate({
|
|
3487
|
+
await applyDocumentUpdate({
|
|
3488
|
+
kind: "order",
|
|
3489
|
+
entity: order,
|
|
3490
|
+
input: parsed,
|
|
3491
|
+
em
|
|
3492
|
+
});
|
|
2770
3493
|
await em.flush();
|
|
2771
3494
|
if (shouldRecalculateTotals) {
|
|
2772
3495
|
const [existingLines, adjustments] = await Promise.all([
|
|
2773
3496
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
2774
|
-
em.find(
|
|
3497
|
+
em.find(
|
|
3498
|
+
SalesOrderAdjustment,
|
|
3499
|
+
{ order },
|
|
3500
|
+
{ orderBy: { position: "asc" } }
|
|
3501
|
+
)
|
|
2775
3502
|
]);
|
|
2776
3503
|
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
2777
3504
|
const calcLines = lineSnapshots.map(
|
|
@@ -2790,7 +3517,9 @@ const updateOrderCommand = {
|
|
|
2790
3517
|
)
|
|
2791
3518
|
);
|
|
2792
3519
|
const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
|
|
2793
|
-
const salesCalculationService = ctx.container.resolve(
|
|
3520
|
+
const salesCalculationService = ctx.container.resolve(
|
|
3521
|
+
"salesCalculationService"
|
|
3522
|
+
);
|
|
2794
3523
|
const calculationContext = buildCalculationContext({
|
|
2795
3524
|
tenantId: order.tenantId,
|
|
2796
3525
|
organizationId: order.organizationId,
|
|
@@ -2802,13 +3531,15 @@ const updateOrderCommand = {
|
|
|
2802
3531
|
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
2803
3532
|
paymentMethodCode: order.paymentMethodCode ?? null
|
|
2804
3533
|
});
|
|
2805
|
-
const calculation = await salesCalculationService.calculateDocumentTotals(
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3534
|
+
const calculation = await salesCalculationService.calculateDocumentTotals(
|
|
3535
|
+
{
|
|
3536
|
+
documentKind: "order",
|
|
3537
|
+
lines: calcLines,
|
|
3538
|
+
adjustments: adjustmentDrafts,
|
|
3539
|
+
context: calculationContext,
|
|
3540
|
+
existingTotals: resolveExistingPaymentTotals(order)
|
|
3541
|
+
}
|
|
3542
|
+
);
|
|
2812
3543
|
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
2813
3544
|
organizationId: order.organizationId,
|
|
2814
3545
|
tenantId: order.tenantId,
|
|
@@ -2870,7 +3601,11 @@ const updateOrderCommand = {
|
|
|
2870
3601
|
await invalidateCrudCache(
|
|
2871
3602
|
ctx.container,
|
|
2872
3603
|
resourceKind,
|
|
2873
|
-
{
|
|
3604
|
+
{
|
|
3605
|
+
id: order.id,
|
|
3606
|
+
organizationId: order.organizationId,
|
|
3607
|
+
tenantId: order.tenantId
|
|
3608
|
+
},
|
|
2874
3609
|
ctx.auth?.tenantId ?? null,
|
|
2875
3610
|
"updated"
|
|
2876
3611
|
);
|
|
@@ -2925,7 +3660,9 @@ const updateOrderCommand = {
|
|
|
2925
3660
|
const createOrderCommand = {
|
|
2926
3661
|
id: "sales.orders.create",
|
|
2927
3662
|
async execute(rawInput, ctx) {
|
|
2928
|
-
const generator = ctx.container.resolve(
|
|
3663
|
+
const generator = ctx.container.resolve(
|
|
3664
|
+
"salesDocumentNumberGenerator"
|
|
3665
|
+
);
|
|
2929
3666
|
const initial = orderCreateSchema.parse(rawInput ?? {});
|
|
2930
3667
|
const orderNumber = typeof initial.orderNumber === "string" && initial.orderNumber.trim().length ? initial.orderNumber.trim() : (await generator.generate({
|
|
2931
3668
|
kind: "order",
|
|
@@ -3059,6 +3796,26 @@ const createOrderCommand = {
|
|
|
3059
3796
|
lineNumber: line.lineNumber ?? index + 1
|
|
3060
3797
|
})
|
|
3061
3798
|
);
|
|
3799
|
+
const uomResolver = createUomResolver();
|
|
3800
|
+
const normalizedLineInputs = await Promise.all(
|
|
3801
|
+
lineInputs.map(async (line) => {
|
|
3802
|
+
const normalized = await normalizeLineUom({
|
|
3803
|
+
em,
|
|
3804
|
+
resolver: uomResolver,
|
|
3805
|
+
organizationId: parsed.organizationId,
|
|
3806
|
+
tenantId: parsed.tenantId,
|
|
3807
|
+
line
|
|
3808
|
+
});
|
|
3809
|
+
return {
|
|
3810
|
+
...line,
|
|
3811
|
+
quantity: normalized.quantity,
|
|
3812
|
+
quantityUnit: normalized.quantityUnit,
|
|
3813
|
+
normalizedQuantity: normalized.normalizedQuantity,
|
|
3814
|
+
normalizedUnit: normalized.normalizedUnit,
|
|
3815
|
+
uomSnapshot: normalized.uomSnapshot
|
|
3816
|
+
};
|
|
3817
|
+
})
|
|
3818
|
+
);
|
|
3062
3819
|
const adjustmentInputs = parsed.adjustments ? parsed.adjustments.map(
|
|
3063
3820
|
(adj) => orderAdjustmentCreateSchema.parse({
|
|
3064
3821
|
...adj,
|
|
@@ -3067,7 +3824,7 @@ const createOrderCommand = {
|
|
|
3067
3824
|
orderId: order.id
|
|
3068
3825
|
})
|
|
3069
3826
|
) : null;
|
|
3070
|
-
const lineSnapshots =
|
|
3827
|
+
const lineSnapshots = normalizedLineInputs.map(
|
|
3071
3828
|
(line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
|
|
3072
3829
|
);
|
|
3073
3830
|
const adjustmentDrafts = adjustmentInputs ? adjustmentInputs.map((adj) => createAdjustmentDraftFromInput(adj)) : [];
|
|
@@ -3090,7 +3847,7 @@ const createOrderCommand = {
|
|
|
3090
3847
|
context: calculationContext,
|
|
3091
3848
|
existingTotals: resolveExistingPaymentTotals(order)
|
|
3092
3849
|
});
|
|
3093
|
-
await replaceOrderLines(em, order, calculation,
|
|
3850
|
+
await replaceOrderLines(em, order, calculation, normalizedLineInputs);
|
|
3094
3851
|
await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
|
|
3095
3852
|
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
3096
3853
|
let eventBus = null;
|
|
@@ -3118,7 +3875,9 @@ const createOrderCommand = {
|
|
|
3118
3875
|
await em.flush();
|
|
3119
3876
|
try {
|
|
3120
3877
|
const notificationService = resolveNotificationService(ctx.container);
|
|
3121
|
-
const typeDef = notificationTypes.find(
|
|
3878
|
+
const typeDef = notificationTypes.find(
|
|
3879
|
+
(type) => type.type === "sales.order.created"
|
|
3880
|
+
);
|
|
3122
3881
|
if (typeDef) {
|
|
3123
3882
|
const totalAmount = order.grandTotalGrossAmount && order.currencyCode ? `${order.grandTotalGrossAmount} ${order.currencyCode}` : "";
|
|
3124
3883
|
const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
|
|
@@ -3139,7 +3898,10 @@ const createOrderCommand = {
|
|
|
3139
3898
|
});
|
|
3140
3899
|
}
|
|
3141
3900
|
} catch (err) {
|
|
3142
|
-
console.error(
|
|
3901
|
+
console.error(
|
|
3902
|
+
"[sales.orders.create] Failed to create notification:",
|
|
3903
|
+
err
|
|
3904
|
+
);
|
|
3143
3905
|
}
|
|
3144
3906
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
3145
3907
|
await emitCrudSideEffects({
|
|
@@ -3158,7 +3920,11 @@ const createOrderCommand = {
|
|
|
3158
3920
|
await invalidateCrudCache(
|
|
3159
3921
|
ctx.container,
|
|
3160
3922
|
resourceKind,
|
|
3161
|
-
{
|
|
3923
|
+
{
|
|
3924
|
+
id: order.id,
|
|
3925
|
+
organizationId: order.organizationId,
|
|
3926
|
+
tenantId: order.tenantId
|
|
3927
|
+
},
|
|
3162
3928
|
ctx.auth?.tenantId ?? null,
|
|
3163
3929
|
"created"
|
|
3164
3930
|
);
|
|
@@ -3207,7 +3973,11 @@ const deleteOrderCommand = {
|
|
|
3207
3973
|
const em = ctx.container.resolve("em");
|
|
3208
3974
|
const snapshot = await loadOrderSnapshot(em, id);
|
|
3209
3975
|
if (snapshot) {
|
|
3210
|
-
ensureOrderScope(
|
|
3976
|
+
ensureOrderScope(
|
|
3977
|
+
ctx,
|
|
3978
|
+
snapshot.order.organizationId,
|
|
3979
|
+
snapshot.order.tenantId
|
|
3980
|
+
);
|
|
3211
3981
|
}
|
|
3212
3982
|
return snapshot ? { before: snapshot } : {};
|
|
3213
3983
|
},
|
|
@@ -3215,29 +3985,56 @@ const deleteOrderCommand = {
|
|
|
3215
3985
|
const id = requireId(input, "Order id is required");
|
|
3216
3986
|
const em = ctx.container.resolve("em").fork();
|
|
3217
3987
|
const order = await em.findOne(SalesOrder, { id });
|
|
3218
|
-
if (!order)
|
|
3988
|
+
if (!order)
|
|
3989
|
+
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
3219
3990
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
3220
3991
|
const shipments = await em.find(SalesShipment, { order: order.id });
|
|
3221
3992
|
const shipmentIds = shipments.map((entry) => entry.id);
|
|
3222
|
-
const [
|
|
3993
|
+
const [
|
|
3994
|
+
shipmentItems,
|
|
3995
|
+
payments,
|
|
3996
|
+
paymentAllocations,
|
|
3997
|
+
addresses,
|
|
3998
|
+
notes,
|
|
3999
|
+
tags,
|
|
4000
|
+
adjustments,
|
|
4001
|
+
lines
|
|
4002
|
+
] = await Promise.all([
|
|
3223
4003
|
shipmentIds.length ? em.find(SalesShipmentItem, { shipment: { $in: shipmentIds } }) : Promise.resolve([]),
|
|
3224
4004
|
em.find(SalesPayment, { order: order.id }),
|
|
3225
4005
|
em.find(SalesPaymentAllocation, { order: order.id }),
|
|
3226
|
-
em.find(SalesDocumentAddress, {
|
|
4006
|
+
em.find(SalesDocumentAddress, {
|
|
4007
|
+
documentId: order.id,
|
|
4008
|
+
documentKind: "order"
|
|
4009
|
+
}),
|
|
3227
4010
|
em.find(SalesNote, { contextType: "order", contextId: order.id }),
|
|
3228
|
-
em.find(SalesDocumentTagAssignment, {
|
|
4011
|
+
em.find(SalesDocumentTagAssignment, {
|
|
4012
|
+
documentId: order.id,
|
|
4013
|
+
documentKind: "order"
|
|
4014
|
+
}),
|
|
3229
4015
|
em.find(SalesOrderAdjustment, { order: order.id }),
|
|
3230
4016
|
em.find(SalesOrderLine, { order: order.id })
|
|
3231
4017
|
]);
|
|
3232
4018
|
if (shipmentIds.length) {
|
|
3233
|
-
await em.nativeDelete(SalesShipmentItem, {
|
|
4019
|
+
await em.nativeDelete(SalesShipmentItem, {
|
|
4020
|
+
shipment: { $in: shipmentIds }
|
|
4021
|
+
});
|
|
3234
4022
|
await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
|
|
3235
4023
|
}
|
|
3236
4024
|
await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
|
|
3237
4025
|
await em.nativeDelete(SalesPayment, { order: order.id });
|
|
3238
|
-
await em.nativeDelete(SalesDocumentAddress, {
|
|
3239
|
-
|
|
3240
|
-
|
|
4026
|
+
await em.nativeDelete(SalesDocumentAddress, {
|
|
4027
|
+
documentId: order.id,
|
|
4028
|
+
documentKind: "order"
|
|
4029
|
+
});
|
|
4030
|
+
await em.nativeDelete(SalesNote, {
|
|
4031
|
+
contextType: "order",
|
|
4032
|
+
contextId: order.id
|
|
4033
|
+
});
|
|
4034
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
4035
|
+
documentId: order.id,
|
|
4036
|
+
documentKind: "order"
|
|
4037
|
+
});
|
|
3241
4038
|
await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
|
|
3242
4039
|
await em.nativeDelete(SalesOrderLine, { order: order.id });
|
|
3243
4040
|
em.remove(order);
|
|
@@ -3246,14 +4043,34 @@ const deleteOrderCommand = {
|
|
|
3246
4043
|
await Promise.all([
|
|
3247
4044
|
queueDeletionSideEffects(dataEngine, order, E.sales.sales_order),
|
|
3248
4045
|
queueDeletionSideEffects(dataEngine, lines, E.sales.sales_order_line),
|
|
3249
|
-
queueDeletionSideEffects(
|
|
4046
|
+
queueDeletionSideEffects(
|
|
4047
|
+
dataEngine,
|
|
4048
|
+
adjustments,
|
|
4049
|
+
E.sales.sales_order_adjustment
|
|
4050
|
+
),
|
|
3250
4051
|
queueDeletionSideEffects(dataEngine, shipments, E.sales.sales_shipment),
|
|
3251
|
-
queueDeletionSideEffects(
|
|
4052
|
+
queueDeletionSideEffects(
|
|
4053
|
+
dataEngine,
|
|
4054
|
+
shipmentItems,
|
|
4055
|
+
E.sales.sales_shipment_item
|
|
4056
|
+
),
|
|
3252
4057
|
queueDeletionSideEffects(dataEngine, payments, E.sales.sales_payment),
|
|
3253
|
-
queueDeletionSideEffects(
|
|
3254
|
-
|
|
4058
|
+
queueDeletionSideEffects(
|
|
4059
|
+
dataEngine,
|
|
4060
|
+
paymentAllocations,
|
|
4061
|
+
E.sales.sales_payment_allocation
|
|
4062
|
+
),
|
|
4063
|
+
queueDeletionSideEffects(
|
|
4064
|
+
dataEngine,
|
|
4065
|
+
addresses,
|
|
4066
|
+
E.sales.sales_document_address
|
|
4067
|
+
),
|
|
3255
4068
|
queueDeletionSideEffects(dataEngine, notes, E.sales.sales_note),
|
|
3256
|
-
queueDeletionSideEffects(
|
|
4069
|
+
queueDeletionSideEffects(
|
|
4070
|
+
dataEngine,
|
|
4071
|
+
tags,
|
|
4072
|
+
E.sales.sales_document_tag_assignment
|
|
4073
|
+
)
|
|
3257
4074
|
]);
|
|
3258
4075
|
return { orderId: id };
|
|
3259
4076
|
},
|
|
@@ -3298,24 +4115,54 @@ const convertQuoteToOrderCommand = {
|
|
|
3298
4115
|
if (!quoteId) return {};
|
|
3299
4116
|
const em = ctx.container.resolve("em");
|
|
3300
4117
|
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
3301
|
-
if (snapshot)
|
|
4118
|
+
if (snapshot)
|
|
4119
|
+
ensureQuoteScope(
|
|
4120
|
+
ctx,
|
|
4121
|
+
snapshot.quote.organizationId,
|
|
4122
|
+
snapshot.quote.tenantId
|
|
4123
|
+
);
|
|
3302
4124
|
return snapshot ? { before: snapshot } : {};
|
|
3303
4125
|
},
|
|
3304
4126
|
async execute(rawInput, ctx) {
|
|
3305
4127
|
const payload = quoteConvertToOrderSchema.parse(rawInput ?? {});
|
|
3306
4128
|
const em = ctx.container.resolve("em").fork();
|
|
3307
|
-
const quote = await em.findOne(SalesQuote, {
|
|
4129
|
+
const quote = await em.findOne(SalesQuote, {
|
|
4130
|
+
id: payload.quoteId,
|
|
4131
|
+
deletedAt: null
|
|
4132
|
+
});
|
|
3308
4133
|
const { translate } = await resolveTranslations();
|
|
3309
|
-
if (!quote)
|
|
4134
|
+
if (!quote)
|
|
4135
|
+
throw new CrudHttpError(404, {
|
|
4136
|
+
error: translate(
|
|
4137
|
+
"sales.documents.detail.error",
|
|
4138
|
+
"Document not found or inaccessible."
|
|
4139
|
+
)
|
|
4140
|
+
});
|
|
3310
4141
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
3311
4142
|
const snapshot = await loadQuoteSnapshot(em, payload.quoteId);
|
|
3312
|
-
if (!snapshot)
|
|
4143
|
+
if (!snapshot)
|
|
4144
|
+
throw new CrudHttpError(404, {
|
|
4145
|
+
error: translate(
|
|
4146
|
+
"sales.documents.detail.error",
|
|
4147
|
+
"Document not found or inaccessible."
|
|
4148
|
+
)
|
|
4149
|
+
});
|
|
3313
4150
|
const orderId = payload.orderId ?? quote.id;
|
|
3314
|
-
const existingOrder = await em.findOne(SalesOrder, {
|
|
4151
|
+
const existingOrder = await em.findOne(SalesOrder, {
|
|
4152
|
+
id: orderId,
|
|
4153
|
+
deletedAt: null
|
|
4154
|
+
});
|
|
3315
4155
|
if (existingOrder) {
|
|
3316
|
-
throw new CrudHttpError(409, {
|
|
4156
|
+
throw new CrudHttpError(409, {
|
|
4157
|
+
error: translate(
|
|
4158
|
+
"sales.documents.detail.convertExists",
|
|
4159
|
+
"Order already exists for this quote."
|
|
4160
|
+
)
|
|
4161
|
+
});
|
|
3317
4162
|
}
|
|
3318
|
-
const generator = ctx.container.resolve(
|
|
4163
|
+
const generator = ctx.container.resolve(
|
|
4164
|
+
"salesDocumentNumberGenerator"
|
|
4165
|
+
);
|
|
3319
4166
|
const generatedNumber = snapshot.quote.quoteNumber && snapshot.quote.quoteNumber.trim().length ? snapshot.quote.quoteNumber : (await generator.generate({
|
|
3320
4167
|
kind: "order",
|
|
3321
4168
|
organizationId: snapshot.quote.organizationId,
|
|
@@ -3328,14 +4175,23 @@ const convertQuoteToOrderCommand = {
|
|
|
3328
4175
|
entityId: E.sales.sales_quote,
|
|
3329
4176
|
recordIds: [snapshot.quote.id],
|
|
3330
4177
|
tenantIdByRecord: { [snapshot.quote.id]: snapshot.quote.tenantId },
|
|
3331
|
-
organizationIdByRecord: {
|
|
4178
|
+
organizationIdByRecord: {
|
|
4179
|
+
[snapshot.quote.id]: snapshot.quote.organizationId
|
|
4180
|
+
}
|
|
3332
4181
|
}),
|
|
3333
4182
|
snapshot.lines.length ? loadCustomFieldValues({
|
|
3334
4183
|
em,
|
|
3335
4184
|
entityId: E.sales.sales_quote_line,
|
|
3336
4185
|
recordIds: snapshot.lines.map((line) => line.id),
|
|
3337
|
-
tenantIdByRecord: Object.fromEntries(
|
|
3338
|
-
|
|
4186
|
+
tenantIdByRecord: Object.fromEntries(
|
|
4187
|
+
snapshot.lines.map((line) => [line.id, snapshot.quote.tenantId])
|
|
4188
|
+
),
|
|
4189
|
+
organizationIdByRecord: Object.fromEntries(
|
|
4190
|
+
snapshot.lines.map((line) => [
|
|
4191
|
+
line.id,
|
|
4192
|
+
snapshot.quote.organizationId
|
|
4193
|
+
])
|
|
4194
|
+
)
|
|
3339
4195
|
}) : Promise.resolve({})
|
|
3340
4196
|
]);
|
|
3341
4197
|
const order = em.create(SalesOrder, {
|
|
@@ -3415,6 +4271,9 @@ const convertQuoteToOrderCommand = {
|
|
|
3415
4271
|
comment: line.comment ?? null,
|
|
3416
4272
|
quantity: line.quantity,
|
|
3417
4273
|
quantityUnit: line.quantityUnit ?? null,
|
|
4274
|
+
normalizedQuantity: line.normalizedQuantity ?? line.quantity,
|
|
4275
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
4276
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
3418
4277
|
reservedQuantity: "0",
|
|
3419
4278
|
fulfilledQuantity: "0",
|
|
3420
4279
|
invoicedQuantity: "0",
|
|
@@ -3466,9 +4325,18 @@ const convertQuoteToOrderCommand = {
|
|
|
3466
4325
|
em.persist(entity);
|
|
3467
4326
|
});
|
|
3468
4327
|
const [addresses, notes, tags] = await Promise.all([
|
|
3469
|
-
em.find(SalesDocumentAddress, {
|
|
3470
|
-
|
|
3471
|
-
|
|
4328
|
+
em.find(SalesDocumentAddress, {
|
|
4329
|
+
documentId: snapshot.quote.id,
|
|
4330
|
+
documentKind: "quote"
|
|
4331
|
+
}),
|
|
4332
|
+
em.find(SalesNote, {
|
|
4333
|
+
contextType: "quote",
|
|
4334
|
+
contextId: snapshot.quote.id
|
|
4335
|
+
}),
|
|
4336
|
+
em.find(SalesDocumentTagAssignment, {
|
|
4337
|
+
documentId: snapshot.quote.id,
|
|
4338
|
+
documentKind: "quote"
|
|
4339
|
+
})
|
|
3472
4340
|
]);
|
|
3473
4341
|
addresses.forEach((entry) => {
|
|
3474
4342
|
entry.documentKind = "order";
|
|
@@ -3531,7 +4399,10 @@ const convertQuoteToOrderCommand = {
|
|
|
3531
4399
|
if (!before) return null;
|
|
3532
4400
|
const { translate } = await resolveTranslations();
|
|
3533
4401
|
return {
|
|
3534
|
-
actionLabel: translate(
|
|
4402
|
+
actionLabel: translate(
|
|
4403
|
+
"sales.audit.quotes.convert",
|
|
4404
|
+
"Convert quote to order"
|
|
4405
|
+
),
|
|
3535
4406
|
resourceKind: "sales.order",
|
|
3536
4407
|
resourceId: result.orderId,
|
|
3537
4408
|
tenantId: before.quote.tenantId,
|
|
@@ -3539,7 +4410,10 @@ const convertQuoteToOrderCommand = {
|
|
|
3539
4410
|
snapshotBefore: before,
|
|
3540
4411
|
snapshotAfter: after ?? null,
|
|
3541
4412
|
payload: {
|
|
3542
|
-
undo: {
|
|
4413
|
+
undo: {
|
|
4414
|
+
quote: before,
|
|
4415
|
+
order: after ?? null
|
|
4416
|
+
}
|
|
3543
4417
|
}
|
|
3544
4418
|
};
|
|
3545
4419
|
},
|
|
@@ -3549,7 +4423,11 @@ const convertQuoteToOrderCommand = {
|
|
|
3549
4423
|
const orderSnapshot = payload?.order;
|
|
3550
4424
|
if (!quoteSnapshot) return;
|
|
3551
4425
|
const em = ctx.container.resolve("em").fork();
|
|
3552
|
-
ensureQuoteScope(
|
|
4426
|
+
ensureQuoteScope(
|
|
4427
|
+
ctx,
|
|
4428
|
+
quoteSnapshot.quote.organizationId,
|
|
4429
|
+
quoteSnapshot.quote.tenantId
|
|
4430
|
+
);
|
|
3553
4431
|
if (orderSnapshot) {
|
|
3554
4432
|
const orderId = orderSnapshot.order.id;
|
|
3555
4433
|
const orderLineIds = orderSnapshot.lines.map((line) => line.id);
|
|
@@ -3558,20 +4436,34 @@ const convertQuoteToOrderCommand = {
|
|
|
3558
4436
|
const shipments = await em.find(SalesShipment, { order: orderId });
|
|
3559
4437
|
const shipmentIds = shipments.map((entry) => entry.id);
|
|
3560
4438
|
if (shipmentIds.length) {
|
|
3561
|
-
await em.nativeDelete(SalesShipmentItem, {
|
|
4439
|
+
await em.nativeDelete(SalesShipmentItem, {
|
|
4440
|
+
shipment: { $in: shipmentIds }
|
|
4441
|
+
});
|
|
3562
4442
|
await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
|
|
3563
4443
|
}
|
|
3564
4444
|
await em.nativeDelete(SalesPaymentAllocation, { order: orderId });
|
|
3565
4445
|
await em.nativeDelete(SalesPayment, { order: orderId });
|
|
3566
|
-
await em.nativeDelete(SalesDocumentAddress, {
|
|
3567
|
-
|
|
4446
|
+
await em.nativeDelete(SalesDocumentAddress, {
|
|
4447
|
+
documentId: orderId,
|
|
4448
|
+
documentKind: "order"
|
|
4449
|
+
});
|
|
4450
|
+
await em.nativeDelete(SalesDocumentTagAssignment, {
|
|
4451
|
+
documentId: orderId,
|
|
4452
|
+
documentKind: "order"
|
|
4453
|
+
});
|
|
3568
4454
|
await em.nativeDelete(SalesOrderAdjustment, { order: orderId });
|
|
3569
4455
|
await em.nativeDelete(SalesOrderLine, { order: orderId });
|
|
3570
4456
|
em.remove(existingOrder);
|
|
3571
4457
|
}
|
|
3572
|
-
await em.nativeDelete(CustomFieldValue, {
|
|
4458
|
+
await em.nativeDelete(CustomFieldValue, {
|
|
4459
|
+
entityId: E.sales.sales_order,
|
|
4460
|
+
recordId: orderId
|
|
4461
|
+
});
|
|
3573
4462
|
if (orderLineIds.length) {
|
|
3574
|
-
await em.nativeDelete(CustomFieldValue, {
|
|
4463
|
+
await em.nativeDelete(CustomFieldValue, {
|
|
4464
|
+
entityId: E.sales.sales_order_line,
|
|
4465
|
+
recordId: { $in: orderLineIds }
|
|
4466
|
+
});
|
|
3575
4467
|
}
|
|
3576
4468
|
}
|
|
3577
4469
|
const noteIds = quoteSnapshot.notes.map((note) => note.id);
|
|
@@ -3582,22 +4474,30 @@ const convertQuoteToOrderCommand = {
|
|
|
3582
4474
|
await em.flush();
|
|
3583
4475
|
}
|
|
3584
4476
|
};
|
|
3585
|
-
const orderLineUpsertSchema = orderLineCreateSchema.extend({
|
|
4477
|
+
const orderLineUpsertSchema = orderLineCreateSchema.extend({
|
|
4478
|
+
id: z.string().uuid().optional()
|
|
4479
|
+
});
|
|
3586
4480
|
const orderLineDeleteSchema = z.object({
|
|
3587
4481
|
id: z.string().uuid(),
|
|
3588
4482
|
orderId: z.string().uuid()
|
|
3589
4483
|
});
|
|
3590
|
-
const quoteLineUpsertSchema = quoteLineCreateSchema.extend({
|
|
4484
|
+
const quoteLineUpsertSchema = quoteLineCreateSchema.extend({
|
|
4485
|
+
id: z.string().uuid().optional()
|
|
4486
|
+
});
|
|
3591
4487
|
const quoteLineDeleteSchema = z.object({
|
|
3592
4488
|
id: z.string().uuid(),
|
|
3593
4489
|
quoteId: z.string().uuid()
|
|
3594
4490
|
});
|
|
3595
|
-
const orderAdjustmentUpsertSchema = orderAdjustmentCreateSchema.extend({
|
|
4491
|
+
const orderAdjustmentUpsertSchema = orderAdjustmentCreateSchema.extend({
|
|
4492
|
+
id: z.string().uuid().optional()
|
|
4493
|
+
});
|
|
3596
4494
|
const orderAdjustmentDeleteSchema = z.object({
|
|
3597
4495
|
id: z.string().uuid(),
|
|
3598
4496
|
orderId: z.string().uuid()
|
|
3599
4497
|
});
|
|
3600
|
-
const quoteAdjustmentUpsertSchema = quoteAdjustmentCreateSchema.extend({
|
|
4498
|
+
const quoteAdjustmentUpsertSchema = quoteAdjustmentCreateSchema.extend({
|
|
4499
|
+
id: z.string().uuid().optional()
|
|
4500
|
+
});
|
|
3601
4501
|
const quoteAdjustmentDeleteSchema = z.object({
|
|
3602
4502
|
id: z.string().uuid(),
|
|
3603
4503
|
quoteId: z.string().uuid()
|
|
@@ -3610,18 +4510,32 @@ const orderLineUpsertCommand = {
|
|
|
3610
4510
|
if (!orderId) return {};
|
|
3611
4511
|
const em = ctx.container.resolve("em");
|
|
3612
4512
|
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
3613
|
-
if (snapshot)
|
|
4513
|
+
if (snapshot)
|
|
4514
|
+
ensureOrderScope(
|
|
4515
|
+
ctx,
|
|
4516
|
+
snapshot.order.organizationId,
|
|
4517
|
+
snapshot.order.tenantId
|
|
4518
|
+
);
|
|
3614
4519
|
return snapshot ? { before: snapshot } : {};
|
|
3615
4520
|
},
|
|
3616
4521
|
async execute(input, ctx) {
|
|
3617
|
-
const
|
|
4522
|
+
const rawBody = input?.body ?? {};
|
|
4523
|
+
const parsed = orderLineUpsertSchema.parse(rawBody);
|
|
3618
4524
|
const em = ctx.container.resolve("em").fork();
|
|
3619
|
-
const order = await em.findOne(SalesOrder, {
|
|
3620
|
-
|
|
4525
|
+
const order = await em.findOne(SalesOrder, {
|
|
4526
|
+
id: parsed.orderId,
|
|
4527
|
+
deletedAt: null
|
|
4528
|
+
});
|
|
4529
|
+
if (!order)
|
|
4530
|
+
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
3621
4531
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
3622
4532
|
const [existingLines, adjustments] = await Promise.all([
|
|
3623
4533
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
3624
|
-
em.find(
|
|
4534
|
+
em.find(
|
|
4535
|
+
SalesOrderAdjustment,
|
|
4536
|
+
{ order },
|
|
4537
|
+
{ orderBy: { position: "asc" } }
|
|
4538
|
+
)
|
|
3625
4539
|
]);
|
|
3626
4540
|
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
3627
4541
|
const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
|
|
@@ -3632,7 +4546,9 @@ const orderLineUpsertCommand = {
|
|
|
3632
4546
|
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
3633
4547
|
let taxService = null;
|
|
3634
4548
|
try {
|
|
3635
|
-
taxService = ctx.container.resolve(
|
|
4549
|
+
taxService = ctx.container.resolve(
|
|
4550
|
+
"taxCalculationService"
|
|
4551
|
+
);
|
|
3636
4552
|
} catch {
|
|
3637
4553
|
taxService = null;
|
|
3638
4554
|
}
|
|
@@ -3655,6 +4571,49 @@ const orderLineUpsertCommand = {
|
|
|
3655
4571
|
if (priceMode) metadata.priceMode = priceMode;
|
|
3656
4572
|
const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
|
|
3657
4573
|
const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
4574
|
+
const lineUomInput = {
|
|
4575
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
4576
|
+
productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
4577
|
+
quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
|
|
4578
|
+
quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
|
|
4579
|
+
normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
|
|
4580
|
+
normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
|
|
4581
|
+
uomSnapshot: existingSnapshot?.uomSnapshot ?? null
|
|
4582
|
+
};
|
|
4583
|
+
const uomResolver = createUomResolver();
|
|
4584
|
+
let normalizedUom = await normalizeLineUom({
|
|
4585
|
+
em,
|
|
4586
|
+
resolver: uomResolver,
|
|
4587
|
+
organizationId: order.organizationId,
|
|
4588
|
+
tenantId: order.tenantId,
|
|
4589
|
+
line: {
|
|
4590
|
+
...lineUomInput,
|
|
4591
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
4592
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
|
|
4593
|
+
}
|
|
4594
|
+
});
|
|
4595
|
+
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
4596
|
+
existingSnapshot,
|
|
4597
|
+
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
4598
|
+
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
4599
|
+
unitPriceNet,
|
|
4600
|
+
unitPriceGross
|
|
4601
|
+
});
|
|
4602
|
+
if (convertedPrices.didConvert) {
|
|
4603
|
+
unitPriceNet = convertedPrices.unitPriceNet;
|
|
4604
|
+
unitPriceGross = convertedPrices.unitPriceGross;
|
|
4605
|
+
normalizedUom = await normalizeLineUom({
|
|
4606
|
+
em,
|
|
4607
|
+
resolver: uomResolver,
|
|
4608
|
+
organizationId: order.organizationId,
|
|
4609
|
+
tenantId: order.tenantId,
|
|
4610
|
+
line: {
|
|
4611
|
+
...lineUomInput,
|
|
4612
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
4613
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
|
|
4614
|
+
}
|
|
4615
|
+
});
|
|
4616
|
+
}
|
|
3658
4617
|
const updatedSnapshot = {
|
|
3659
4618
|
id: lineId,
|
|
3660
4619
|
lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
|
|
@@ -3664,8 +4623,11 @@ const orderLineUpsertCommand = {
|
|
|
3664
4623
|
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
3665
4624
|
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
3666
4625
|
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
3667
|
-
quantity:
|
|
3668
|
-
quantityUnit:
|
|
4626
|
+
quantity: normalizedUom.quantity,
|
|
4627
|
+
quantityUnit: normalizedUom.quantityUnit,
|
|
4628
|
+
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
4629
|
+
normalizedUnit: normalizedUom.normalizedUnit,
|
|
4630
|
+
uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
|
|
3669
4631
|
currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? order.currencyCode,
|
|
3670
4632
|
unitPriceNet: unitPriceNet ?? 0,
|
|
3671
4633
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
@@ -3684,7 +4646,9 @@ const orderLineUpsertCommand = {
|
|
|
3684
4646
|
updatedSnapshot.statusEntryId = statusEntryId;
|
|
3685
4647
|
updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
|
|
3686
4648
|
updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
|
|
3687
|
-
let nextLines = parsed.id ? lineSnapshots.map(
|
|
4649
|
+
let nextLines = parsed.id ? lineSnapshots.map(
|
|
4650
|
+
(line) => line.id === parsed.id ? updatedSnapshot : line
|
|
4651
|
+
) : [...lineSnapshots, updatedSnapshot];
|
|
3688
4652
|
nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
|
|
3689
4653
|
const sourceInputs = nextLines.map((line, index) => ({
|
|
3690
4654
|
...line,
|
|
@@ -3755,7 +4719,10 @@ const orderLineUpsertCommand = {
|
|
|
3755
4719
|
if (!after) return null;
|
|
3756
4720
|
const { translate } = await resolveTranslations();
|
|
3757
4721
|
return {
|
|
3758
|
-
actionLabel: translate(
|
|
4722
|
+
actionLabel: translate(
|
|
4723
|
+
"sales.audit.orders.lines.upsert",
|
|
4724
|
+
"Upsert order line"
|
|
4725
|
+
),
|
|
3759
4726
|
resourceKind: "sales.order",
|
|
3760
4727
|
resourceId: result.orderId,
|
|
3761
4728
|
tenantId: after.order.tenantId,
|
|
@@ -3785,15 +4752,31 @@ const orderLineDeleteCommand = {
|
|
|
3785
4752
|
if (!orderId) return {};
|
|
3786
4753
|
const em = ctx.container.resolve("em");
|
|
3787
4754
|
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
3788
|
-
if (snapshot)
|
|
4755
|
+
if (snapshot)
|
|
4756
|
+
ensureOrderScope(
|
|
4757
|
+
ctx,
|
|
4758
|
+
snapshot.order.organizationId,
|
|
4759
|
+
snapshot.order.tenantId
|
|
4760
|
+
);
|
|
3789
4761
|
return snapshot ? { before: snapshot } : {};
|
|
3790
4762
|
},
|
|
3791
4763
|
async execute(input, ctx) {
|
|
3792
4764
|
const { translate } = await resolveTranslations();
|
|
3793
|
-
const parsed = orderLineDeleteSchema.parse(
|
|
4765
|
+
const parsed = orderLineDeleteSchema.parse(
|
|
4766
|
+
input?.body ?? {}
|
|
4767
|
+
);
|
|
3794
4768
|
const em = ctx.container.resolve("em").fork();
|
|
3795
|
-
const order = await em.findOne(SalesOrder, {
|
|
3796
|
-
|
|
4769
|
+
const order = await em.findOne(SalesOrder, {
|
|
4770
|
+
id: parsed.orderId,
|
|
4771
|
+
deletedAt: null
|
|
4772
|
+
});
|
|
4773
|
+
if (!order)
|
|
4774
|
+
throw new CrudHttpError(404, {
|
|
4775
|
+
error: translate(
|
|
4776
|
+
"sales.documents.detail.error",
|
|
4777
|
+
"Document not found or inaccessible."
|
|
4778
|
+
)
|
|
4779
|
+
});
|
|
3797
4780
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
3798
4781
|
const shipmentCount = await em.count(SalesShipmentItem, {
|
|
3799
4782
|
orderLine: parsed.id,
|
|
@@ -3807,11 +4790,24 @@ const orderLineDeleteCommand = {
|
|
|
3807
4790
|
)
|
|
3808
4791
|
});
|
|
3809
4792
|
}
|
|
3810
|
-
const existingLines = await em.find(
|
|
3811
|
-
|
|
4793
|
+
const existingLines = await em.find(
|
|
4794
|
+
SalesOrderLine,
|
|
4795
|
+
{ order },
|
|
4796
|
+
{ orderBy: { lineNumber: "asc" } }
|
|
4797
|
+
);
|
|
4798
|
+
const adjustments = await em.find(
|
|
4799
|
+
SalesOrderAdjustment,
|
|
4800
|
+
{ order },
|
|
4801
|
+
{ orderBy: { position: "asc" } }
|
|
4802
|
+
);
|
|
3812
4803
|
const filtered = existingLines.filter((line) => line.id !== parsed.id);
|
|
3813
4804
|
if (filtered.length === existingLines.length) {
|
|
3814
|
-
throw new CrudHttpError(404, {
|
|
4805
|
+
throw new CrudHttpError(404, {
|
|
4806
|
+
error: translate(
|
|
4807
|
+
"sales.documents.detail.error",
|
|
4808
|
+
"Document not found or inaccessible."
|
|
4809
|
+
)
|
|
4810
|
+
});
|
|
3815
4811
|
}
|
|
3816
4812
|
const sourceInputs = filtered.map((line, index) => ({
|
|
3817
4813
|
...mapOrderLineEntityToSnapshot(line),
|
|
@@ -3882,7 +4878,10 @@ const orderLineDeleteCommand = {
|
|
|
3882
4878
|
if (!after) return null;
|
|
3883
4879
|
const { translate } = await resolveTranslations();
|
|
3884
4880
|
return {
|
|
3885
|
-
actionLabel: translate(
|
|
4881
|
+
actionLabel: translate(
|
|
4882
|
+
"sales.audit.orders.lines.delete",
|
|
4883
|
+
"Delete order line"
|
|
4884
|
+
),
|
|
3886
4885
|
resourceKind: "sales.order",
|
|
3887
4886
|
resourceId: result.orderId,
|
|
3888
4887
|
tenantId: after.order.tenantId,
|
|
@@ -3912,18 +4911,32 @@ const quoteLineUpsertCommand = {
|
|
|
3912
4911
|
if (!quoteId) return {};
|
|
3913
4912
|
const em = ctx.container.resolve("em");
|
|
3914
4913
|
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
3915
|
-
if (snapshot)
|
|
4914
|
+
if (snapshot)
|
|
4915
|
+
ensureQuoteScope(
|
|
4916
|
+
ctx,
|
|
4917
|
+
snapshot.quote.organizationId,
|
|
4918
|
+
snapshot.quote.tenantId
|
|
4919
|
+
);
|
|
3916
4920
|
return snapshot ? { before: snapshot } : {};
|
|
3917
4921
|
},
|
|
3918
4922
|
async execute(input, ctx) {
|
|
3919
|
-
const
|
|
4923
|
+
const rawBody = input?.body ?? {};
|
|
4924
|
+
const parsed = quoteLineUpsertSchema.parse(rawBody);
|
|
3920
4925
|
const em = ctx.container.resolve("em").fork();
|
|
3921
|
-
const quote = await em.findOne(SalesQuote, {
|
|
3922
|
-
|
|
4926
|
+
const quote = await em.findOne(SalesQuote, {
|
|
4927
|
+
id: parsed.quoteId,
|
|
4928
|
+
deletedAt: null
|
|
4929
|
+
});
|
|
4930
|
+
if (!quote)
|
|
4931
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
3923
4932
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
3924
4933
|
const [existingLines, adjustments] = await Promise.all([
|
|
3925
4934
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
3926
|
-
em.find(
|
|
4935
|
+
em.find(
|
|
4936
|
+
SalesQuoteAdjustment,
|
|
4937
|
+
{ quote },
|
|
4938
|
+
{ orderBy: { position: "asc" } }
|
|
4939
|
+
)
|
|
3927
4940
|
]);
|
|
3928
4941
|
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
3929
4942
|
const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
|
|
@@ -3934,7 +4947,9 @@ const quoteLineUpsertCommand = {
|
|
|
3934
4947
|
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
3935
4948
|
let taxService = null;
|
|
3936
4949
|
try {
|
|
3937
|
-
taxService = ctx.container.resolve(
|
|
4950
|
+
taxService = ctx.container.resolve(
|
|
4951
|
+
"taxCalculationService"
|
|
4952
|
+
);
|
|
3938
4953
|
} catch {
|
|
3939
4954
|
taxService = null;
|
|
3940
4955
|
}
|
|
@@ -3957,6 +4972,49 @@ const quoteLineUpsertCommand = {
|
|
|
3957
4972
|
if (priceMode) metadata.priceMode = priceMode;
|
|
3958
4973
|
const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
|
|
3959
4974
|
const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
|
|
4975
|
+
const lineUomInput = {
|
|
4976
|
+
productId: parsed.productId ?? existingSnapshot?.productId ?? null,
|
|
4977
|
+
productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
|
|
4978
|
+
quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
|
|
4979
|
+
quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
|
|
4980
|
+
normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
|
|
4981
|
+
normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
|
|
4982
|
+
uomSnapshot: existingSnapshot?.uomSnapshot ?? null
|
|
4983
|
+
};
|
|
4984
|
+
const uomResolver = createUomResolver();
|
|
4985
|
+
let normalizedUom = await normalizeLineUom({
|
|
4986
|
+
em,
|
|
4987
|
+
resolver: uomResolver,
|
|
4988
|
+
organizationId: quote.organizationId,
|
|
4989
|
+
tenantId: quote.tenantId,
|
|
4990
|
+
line: {
|
|
4991
|
+
...lineUomInput,
|
|
4992
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
4993
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4996
|
+
const convertedPrices = convertLineUnitPricesOnUnitChange({
|
|
4997
|
+
existingSnapshot,
|
|
4998
|
+
nextQuantityUnit: normalizedUom.quantityUnit,
|
|
4999
|
+
nextUomSnapshot: normalizedUom.uomSnapshot,
|
|
5000
|
+
unitPriceNet,
|
|
5001
|
+
unitPriceGross
|
|
5002
|
+
});
|
|
5003
|
+
if (convertedPrices.didConvert) {
|
|
5004
|
+
unitPriceNet = convertedPrices.unitPriceNet;
|
|
5005
|
+
unitPriceGross = convertedPrices.unitPriceGross;
|
|
5006
|
+
normalizedUom = await normalizeLineUom({
|
|
5007
|
+
em,
|
|
5008
|
+
resolver: uomResolver,
|
|
5009
|
+
organizationId: quote.organizationId,
|
|
5010
|
+
tenantId: quote.tenantId,
|
|
5011
|
+
line: {
|
|
5012
|
+
...lineUomInput,
|
|
5013
|
+
unitPriceNet: unitPriceNet ?? 0,
|
|
5014
|
+
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
|
|
5015
|
+
}
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
3960
5018
|
const updatedSnapshot = {
|
|
3961
5019
|
id: lineId,
|
|
3962
5020
|
lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
|
|
@@ -3966,8 +5024,11 @@ const quoteLineUpsertCommand = {
|
|
|
3966
5024
|
name: parsed.name ?? existingSnapshot?.name ?? null,
|
|
3967
5025
|
description: parsed.description ?? existingSnapshot?.description ?? null,
|
|
3968
5026
|
comment: parsed.comment ?? existingSnapshot?.comment ?? null,
|
|
3969
|
-
quantity:
|
|
3970
|
-
quantityUnit:
|
|
5027
|
+
quantity: normalizedUom.quantity,
|
|
5028
|
+
quantityUnit: normalizedUom.quantityUnit,
|
|
5029
|
+
normalizedQuantity: normalizedUom.normalizedQuantity,
|
|
5030
|
+
normalizedUnit: normalizedUom.normalizedUnit,
|
|
5031
|
+
uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
|
|
3971
5032
|
currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? quote.currencyCode,
|
|
3972
5033
|
unitPriceNet: unitPriceNet ?? 0,
|
|
3973
5034
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
@@ -3986,7 +5047,9 @@ const quoteLineUpsertCommand = {
|
|
|
3986
5047
|
updatedSnapshot.statusEntryId = statusEntryId;
|
|
3987
5048
|
updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
|
|
3988
5049
|
updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
|
|
3989
|
-
let nextLines = parsed.id ? lineSnapshots.map(
|
|
5050
|
+
let nextLines = parsed.id ? lineSnapshots.map(
|
|
5051
|
+
(line) => line.id === parsed.id ? updatedSnapshot : line
|
|
5052
|
+
) : [...lineSnapshots, updatedSnapshot];
|
|
3990
5053
|
nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
|
|
3991
5054
|
const sourceInputs = nextLines.map((line, index) => ({
|
|
3992
5055
|
...line,
|
|
@@ -4056,7 +5119,10 @@ const quoteLineUpsertCommand = {
|
|
|
4056
5119
|
if (!after) return null;
|
|
4057
5120
|
const { translate } = await resolveTranslations();
|
|
4058
5121
|
return {
|
|
4059
|
-
actionLabel: translate(
|
|
5122
|
+
actionLabel: translate(
|
|
5123
|
+
"sales.audit.quotes.lines.upsert",
|
|
5124
|
+
"Upsert quote line"
|
|
5125
|
+
),
|
|
4060
5126
|
resourceKind: "sales.quote",
|
|
4061
5127
|
resourceId: result.quoteId,
|
|
4062
5128
|
tenantId: after.quote.tenantId,
|
|
@@ -4086,17 +5152,36 @@ const quoteLineDeleteCommand = {
|
|
|
4086
5152
|
if (!quoteId) return {};
|
|
4087
5153
|
const em = ctx.container.resolve("em");
|
|
4088
5154
|
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
4089
|
-
if (snapshot)
|
|
5155
|
+
if (snapshot)
|
|
5156
|
+
ensureQuoteScope(
|
|
5157
|
+
ctx,
|
|
5158
|
+
snapshot.quote.organizationId,
|
|
5159
|
+
snapshot.quote.tenantId
|
|
5160
|
+
);
|
|
4090
5161
|
return snapshot ? { before: snapshot } : {};
|
|
4091
5162
|
},
|
|
4092
5163
|
async execute(input, ctx) {
|
|
4093
|
-
const parsed = quoteLineDeleteSchema.parse(
|
|
5164
|
+
const parsed = quoteLineDeleteSchema.parse(
|
|
5165
|
+
input?.body ?? {}
|
|
5166
|
+
);
|
|
4094
5167
|
const em = ctx.container.resolve("em").fork();
|
|
4095
|
-
const quote = await em.findOne(SalesQuote, {
|
|
4096
|
-
|
|
5168
|
+
const quote = await em.findOne(SalesQuote, {
|
|
5169
|
+
id: parsed.quoteId,
|
|
5170
|
+
deletedAt: null
|
|
5171
|
+
});
|
|
5172
|
+
if (!quote)
|
|
5173
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
4097
5174
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
4098
|
-
const existingLines = await em.find(
|
|
4099
|
-
|
|
5175
|
+
const existingLines = await em.find(
|
|
5176
|
+
SalesQuoteLine,
|
|
5177
|
+
{ quote },
|
|
5178
|
+
{ orderBy: { lineNumber: "asc" } }
|
|
5179
|
+
);
|
|
5180
|
+
const adjustments = await em.find(
|
|
5181
|
+
SalesQuoteAdjustment,
|
|
5182
|
+
{ quote },
|
|
5183
|
+
{ orderBy: { position: "asc" } }
|
|
5184
|
+
);
|
|
4100
5185
|
const filtered = existingLines.filter((line) => line.id !== parsed.id);
|
|
4101
5186
|
if (filtered.length === existingLines.length) {
|
|
4102
5187
|
throw new CrudHttpError(404, { error: "Quote line not found" });
|
|
@@ -4169,7 +5254,10 @@ const quoteLineDeleteCommand = {
|
|
|
4169
5254
|
if (!after) return null;
|
|
4170
5255
|
const { translate } = await resolveTranslations();
|
|
4171
5256
|
return {
|
|
4172
|
-
actionLabel: translate(
|
|
5257
|
+
actionLabel: translate(
|
|
5258
|
+
"sales.audit.quotes.lines.delete",
|
|
5259
|
+
"Delete quote line"
|
|
5260
|
+
),
|
|
4173
5261
|
resourceKind: "sales.quote",
|
|
4174
5262
|
resourceId: result.quoteId,
|
|
4175
5263
|
tenantId: after.quote.tenantId,
|
|
@@ -4199,21 +5287,38 @@ const orderAdjustmentUpsertCommand = {
|
|
|
4199
5287
|
if (!orderId) return {};
|
|
4200
5288
|
const em = ctx.container.resolve("em");
|
|
4201
5289
|
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
4202
|
-
if (snapshot)
|
|
5290
|
+
if (snapshot)
|
|
5291
|
+
ensureOrderScope(
|
|
5292
|
+
ctx,
|
|
5293
|
+
snapshot.order.organizationId,
|
|
5294
|
+
snapshot.order.tenantId
|
|
5295
|
+
);
|
|
4203
5296
|
return snapshot ? { before: snapshot } : {};
|
|
4204
5297
|
},
|
|
4205
5298
|
async execute(input, ctx) {
|
|
4206
|
-
const parsed = orderAdjustmentUpsertSchema.parse(
|
|
5299
|
+
const parsed = orderAdjustmentUpsertSchema.parse(
|
|
5300
|
+
input?.body ?? {}
|
|
5301
|
+
);
|
|
4207
5302
|
const em = ctx.container.resolve("em").fork();
|
|
4208
|
-
const order = await em.findOne(SalesOrder, {
|
|
4209
|
-
|
|
5303
|
+
const order = await em.findOne(SalesOrder, {
|
|
5304
|
+
id: parsed.orderId,
|
|
5305
|
+
deletedAt: null
|
|
5306
|
+
});
|
|
5307
|
+
if (!order)
|
|
5308
|
+
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
4210
5309
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
4211
5310
|
if (parsed.scope === "line") {
|
|
4212
|
-
throw new CrudHttpError(400, {
|
|
5311
|
+
throw new CrudHttpError(400, {
|
|
5312
|
+
error: "Line-scoped adjustments are not supported yet."
|
|
5313
|
+
});
|
|
4213
5314
|
}
|
|
4214
5315
|
const [existingLines, existingAdjustments] = await Promise.all([
|
|
4215
5316
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
4216
|
-
em.find(
|
|
5317
|
+
em.find(
|
|
5318
|
+
SalesOrderAdjustment,
|
|
5319
|
+
{ order },
|
|
5320
|
+
{ orderBy: { position: "asc" } }
|
|
5321
|
+
)
|
|
4217
5322
|
]);
|
|
4218
5323
|
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
4219
5324
|
const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
|
|
@@ -4343,7 +5448,10 @@ const orderAdjustmentUpsertCommand = {
|
|
|
4343
5448
|
if (!after) return null;
|
|
4344
5449
|
const { translate } = await resolveTranslations();
|
|
4345
5450
|
return {
|
|
4346
|
-
actionLabel: translate(
|
|
5451
|
+
actionLabel: translate(
|
|
5452
|
+
"sales.audit.orders.adjustments.upsert",
|
|
5453
|
+
"Upsert order adjustment"
|
|
5454
|
+
),
|
|
4347
5455
|
resourceKind: "sales.order",
|
|
4348
5456
|
resourceId: result.orderId,
|
|
4349
5457
|
tenantId: after.order.tenantId,
|
|
@@ -4373,18 +5481,33 @@ const orderAdjustmentDeleteCommand = {
|
|
|
4373
5481
|
if (!orderId) return {};
|
|
4374
5482
|
const em = ctx.container.resolve("em");
|
|
4375
5483
|
const snapshot = await loadOrderSnapshot(em, orderId);
|
|
4376
|
-
if (snapshot)
|
|
5484
|
+
if (snapshot)
|
|
5485
|
+
ensureOrderScope(
|
|
5486
|
+
ctx,
|
|
5487
|
+
snapshot.order.organizationId,
|
|
5488
|
+
snapshot.order.tenantId
|
|
5489
|
+
);
|
|
4377
5490
|
return snapshot ? { before: snapshot } : {};
|
|
4378
5491
|
},
|
|
4379
5492
|
async execute(input, ctx) {
|
|
4380
|
-
const parsed = orderAdjustmentDeleteSchema.parse(
|
|
5493
|
+
const parsed = orderAdjustmentDeleteSchema.parse(
|
|
5494
|
+
input?.body ?? {}
|
|
5495
|
+
);
|
|
4381
5496
|
const em = ctx.container.resolve("em").fork();
|
|
4382
|
-
const order = await em.findOne(SalesOrder, {
|
|
4383
|
-
|
|
5497
|
+
const order = await em.findOne(SalesOrder, {
|
|
5498
|
+
id: parsed.orderId,
|
|
5499
|
+
deletedAt: null
|
|
5500
|
+
});
|
|
5501
|
+
if (!order)
|
|
5502
|
+
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
4384
5503
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
4385
5504
|
const [existingLines, adjustments] = await Promise.all([
|
|
4386
5505
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
4387
|
-
em.find(
|
|
5506
|
+
em.find(
|
|
5507
|
+
SalesOrderAdjustment,
|
|
5508
|
+
{ order },
|
|
5509
|
+
{ orderBy: { position: "asc" } }
|
|
5510
|
+
)
|
|
4388
5511
|
]);
|
|
4389
5512
|
const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
|
|
4390
5513
|
if (filtered.length === adjustments.length) {
|
|
@@ -4474,7 +5597,10 @@ const orderAdjustmentDeleteCommand = {
|
|
|
4474
5597
|
if (!after) return null;
|
|
4475
5598
|
const { translate } = await resolveTranslations();
|
|
4476
5599
|
return {
|
|
4477
|
-
actionLabel: translate(
|
|
5600
|
+
actionLabel: translate(
|
|
5601
|
+
"sales.audit.orders.adjustments.delete",
|
|
5602
|
+
"Delete order adjustment"
|
|
5603
|
+
),
|
|
4478
5604
|
resourceKind: "sales.order",
|
|
4479
5605
|
resourceId: result.orderId,
|
|
4480
5606
|
tenantId: after.order.tenantId,
|
|
@@ -4504,21 +5630,38 @@ const quoteAdjustmentUpsertCommand = {
|
|
|
4504
5630
|
if (!quoteId) return {};
|
|
4505
5631
|
const em = ctx.container.resolve("em");
|
|
4506
5632
|
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
4507
|
-
if (snapshot)
|
|
5633
|
+
if (snapshot)
|
|
5634
|
+
ensureQuoteScope(
|
|
5635
|
+
ctx,
|
|
5636
|
+
snapshot.quote.organizationId,
|
|
5637
|
+
snapshot.quote.tenantId
|
|
5638
|
+
);
|
|
4508
5639
|
return snapshot ? { before: snapshot } : {};
|
|
4509
5640
|
},
|
|
4510
5641
|
async execute(input, ctx) {
|
|
4511
|
-
const parsed = quoteAdjustmentUpsertSchema.parse(
|
|
5642
|
+
const parsed = quoteAdjustmentUpsertSchema.parse(
|
|
5643
|
+
input?.body ?? {}
|
|
5644
|
+
);
|
|
4512
5645
|
const em = ctx.container.resolve("em").fork();
|
|
4513
|
-
const quote = await em.findOne(SalesQuote, {
|
|
4514
|
-
|
|
5646
|
+
const quote = await em.findOne(SalesQuote, {
|
|
5647
|
+
id: parsed.quoteId,
|
|
5648
|
+
deletedAt: null
|
|
5649
|
+
});
|
|
5650
|
+
if (!quote)
|
|
5651
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
4515
5652
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
4516
5653
|
if (parsed.scope === "line") {
|
|
4517
|
-
throw new CrudHttpError(400, {
|
|
5654
|
+
throw new CrudHttpError(400, {
|
|
5655
|
+
error: "Line-scoped adjustments are not supported yet."
|
|
5656
|
+
});
|
|
4518
5657
|
}
|
|
4519
5658
|
const [existingLines, existingAdjustments] = await Promise.all([
|
|
4520
5659
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
4521
|
-
em.find(
|
|
5660
|
+
em.find(
|
|
5661
|
+
SalesQuoteAdjustment,
|
|
5662
|
+
{ quote },
|
|
5663
|
+
{ orderBy: { position: "asc" } }
|
|
5664
|
+
)
|
|
4522
5665
|
]);
|
|
4523
5666
|
const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
|
|
4524
5667
|
const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
|
|
@@ -4647,7 +5790,10 @@ const quoteAdjustmentUpsertCommand = {
|
|
|
4647
5790
|
if (!after) return null;
|
|
4648
5791
|
const { translate } = await resolveTranslations();
|
|
4649
5792
|
return {
|
|
4650
|
-
actionLabel: translate(
|
|
5793
|
+
actionLabel: translate(
|
|
5794
|
+
"sales.audit.quotes.adjustments.upsert",
|
|
5795
|
+
"Upsert quote adjustment"
|
|
5796
|
+
),
|
|
4651
5797
|
resourceKind: "sales.quote",
|
|
4652
5798
|
resourceId: result.quoteId,
|
|
4653
5799
|
tenantId: after.quote.tenantId,
|
|
@@ -4677,18 +5823,33 @@ const quoteAdjustmentDeleteCommand = {
|
|
|
4677
5823
|
if (!quoteId) return {};
|
|
4678
5824
|
const em = ctx.container.resolve("em");
|
|
4679
5825
|
const snapshot = await loadQuoteSnapshot(em, quoteId);
|
|
4680
|
-
if (snapshot)
|
|
5826
|
+
if (snapshot)
|
|
5827
|
+
ensureQuoteScope(
|
|
5828
|
+
ctx,
|
|
5829
|
+
snapshot.quote.organizationId,
|
|
5830
|
+
snapshot.quote.tenantId
|
|
5831
|
+
);
|
|
4681
5832
|
return snapshot ? { before: snapshot } : {};
|
|
4682
5833
|
},
|
|
4683
5834
|
async execute(input, ctx) {
|
|
4684
|
-
const parsed = quoteAdjustmentDeleteSchema.parse(
|
|
5835
|
+
const parsed = quoteAdjustmentDeleteSchema.parse(
|
|
5836
|
+
input?.body ?? {}
|
|
5837
|
+
);
|
|
4685
5838
|
const em = ctx.container.resolve("em").fork();
|
|
4686
|
-
const quote = await em.findOne(SalesQuote, {
|
|
4687
|
-
|
|
5839
|
+
const quote = await em.findOne(SalesQuote, {
|
|
5840
|
+
id: parsed.quoteId,
|
|
5841
|
+
deletedAt: null
|
|
5842
|
+
});
|
|
5843
|
+
if (!quote)
|
|
5844
|
+
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
4688
5845
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
4689
5846
|
const [existingLines, adjustments] = await Promise.all([
|
|
4690
5847
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
4691
|
-
em.find(
|
|
5848
|
+
em.find(
|
|
5849
|
+
SalesQuoteAdjustment,
|
|
5850
|
+
{ quote },
|
|
5851
|
+
{ orderBy: { position: "asc" } }
|
|
5852
|
+
)
|
|
4692
5853
|
]);
|
|
4693
5854
|
const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
|
|
4694
5855
|
if (filtered.length === adjustments.length) {
|
|
@@ -4777,7 +5938,10 @@ const quoteAdjustmentDeleteCommand = {
|
|
|
4777
5938
|
if (!after) return null;
|
|
4778
5939
|
const { translate } = await resolveTranslations();
|
|
4779
5940
|
return {
|
|
4780
|
-
actionLabel: translate(
|
|
5941
|
+
actionLabel: translate(
|
|
5942
|
+
"sales.audit.quotes.adjustments.delete",
|
|
5943
|
+
"Delete quote adjustment"
|
|
5944
|
+
),
|
|
4781
5945
|
resourceKind: "sales.quote",
|
|
4782
5946
|
resourceId: result.quoteId,
|
|
4783
5947
|
tenantId: after.quote.tenantId,
|