@open-mercato/core 0.10.1-develop.7723.1.3b2ea82ea7 → 0.10.1-develop.7724.1.95e4e7d38e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/sales_order/index.js +2 -0
- package/dist/generated/entities/sales_order/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +2 -0
- package/dist/generated/entities/sales_order_line/index.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/sales/api/documents/factory.js +3 -0
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +2 -1
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +141 -18
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +24 -7
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
- package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +10 -0
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +9 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +85 -6
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/externalAmounts.js +143 -0
- package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
- package/dist/modules/sales/lib/lineSnapshots.js +6 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sales/lib/providers/totals.js +3 -2
- package/dist/modules/sales/lib/providers/totals.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
- package/generated/entities/sales_order/index.ts +1 -0
- package/generated/entities/sales_order_line/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/sales/api/documents/factory.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +2 -1
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
- package/src/modules/sales/commands/documents.ts +219 -44
- package/src/modules/sales/commands/returns.ts +27 -3
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
- package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
- package/src/modules/sales/data/entities.ts +14 -0
- package/src/modules/sales/data/validators.ts +10 -0
- package/src/modules/sales/i18n/de.json +12 -0
- package/src/modules/sales/i18n/en.json +12 -0
- package/src/modules/sales/i18n/es.json +12 -0
- package/src/modules/sales/i18n/ko.json +12 -0
- package/src/modules/sales/i18n/pl.json +12 -0
- package/src/modules/sales/lib/calculations.ts +99 -4
- package/src/modules/sales/lib/externalAmounts.ts +174 -0
- package/src/modules/sales/lib/lineSnapshots.ts +6 -1
- package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
- package/src/modules/sales/lib/providers/totals.ts +5 -2
- package/src/modules/sales/lib/types.ts +19 -2
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
- package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
|
@@ -42,6 +42,8 @@ type SalesDocumentAdjustmentsSectionProps = {
|
|
|
42
42
|
tenantId?: string | null
|
|
43
43
|
onActionChange?: (action: SectionAction | null) => void
|
|
44
44
|
onRowsChange?: (rows: AdjustmentRow[]) => void
|
|
45
|
+
/** Withdraws adjustment writes: on an external order they would all be refused. */
|
|
46
|
+
amountsReadOnly?: boolean
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
const FALLBACK_ADJUSTMENT_KIND_VALUES: SalesAdjustmentKind[] = [
|
|
@@ -76,6 +78,7 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
76
78
|
tenantId: tenantFromProps,
|
|
77
79
|
onActionChange,
|
|
78
80
|
onRowsChange,
|
|
81
|
+
amountsReadOnly = false,
|
|
79
82
|
}: SalesDocumentAdjustmentsSectionProps) {
|
|
80
83
|
const t = useT()
|
|
81
84
|
const runDocumentMutation = useSalesDocumentMutation(kind, documentId)
|
|
@@ -281,10 +284,14 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
281
284
|
setDialogOpen(true)
|
|
282
285
|
}, [])
|
|
283
286
|
|
|
284
|
-
const handleEdit = React.useCallback(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
const handleEdit = React.useCallback(
|
|
288
|
+
(row: AdjustmentRow) => {
|
|
289
|
+
if (amountsReadOnly) return
|
|
290
|
+
setActiveAdjustment(row)
|
|
291
|
+
setDialogOpen(true)
|
|
292
|
+
},
|
|
293
|
+
[amountsReadOnly]
|
|
294
|
+
)
|
|
288
295
|
|
|
289
296
|
const handleCloseDialog = React.useCallback(() => {
|
|
290
297
|
setDialogOpen(false)
|
|
@@ -296,10 +303,10 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
296
303
|
onActionChange({
|
|
297
304
|
label: t('sales.documents.adjustments.add', 'Add adjustment'),
|
|
298
305
|
onClick: handleOpenCreate,
|
|
299
|
-
disabled:
|
|
306
|
+
disabled: amountsReadOnly,
|
|
300
307
|
})
|
|
301
308
|
return () => onActionChange(null)
|
|
302
|
-
}, [handleOpenCreate, onActionChange, t])
|
|
309
|
+
}, [amountsReadOnly, handleOpenCreate, onActionChange, t])
|
|
303
310
|
|
|
304
311
|
const handleFormSubmit = React.useCallback(
|
|
305
312
|
async (values: AdjustmentSubmitPayload) => {
|
|
@@ -451,24 +458,25 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
451
458
|
const showLoadingState = loading && rows.length === 0
|
|
452
459
|
|
|
453
460
|
const renderRowActions = React.useCallback(
|
|
454
|
-
(row: AdjustmentRow) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
461
|
+
(row: AdjustmentRow) =>
|
|
462
|
+
amountsReadOnly ? null : (
|
|
463
|
+
<RowActions
|
|
464
|
+
items={[
|
|
465
|
+
{
|
|
466
|
+
id: 'edit',
|
|
467
|
+
label: t('ui.actions.edit', 'Edit'),
|
|
468
|
+
onSelect: () => handleEdit(row),
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: 'delete',
|
|
472
|
+
label: t('ui.actions.delete', 'Delete'),
|
|
473
|
+
destructive: true,
|
|
474
|
+
onSelect: () => handleDelete(row),
|
|
475
|
+
},
|
|
476
|
+
]}
|
|
477
|
+
/>
|
|
478
|
+
),
|
|
479
|
+
[amountsReadOnly, handleDelete, handleEdit, t]
|
|
472
480
|
)
|
|
473
481
|
|
|
474
482
|
return (
|
|
@@ -487,7 +495,7 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
487
495
|
columns={columns}
|
|
488
496
|
isLoading={loading && rows.length > 0}
|
|
489
497
|
embedded
|
|
490
|
-
onRowClick={handleEdit}
|
|
498
|
+
onRowClick={amountsReadOnly ? undefined : handleEdit}
|
|
491
499
|
rowActions={renderRowActions}
|
|
492
500
|
emptyState={
|
|
493
501
|
<TabEmptyState
|
|
@@ -495,6 +503,7 @@ export function SalesDocumentAdjustmentsSection({
|
|
|
495
503
|
description={t('sales.documents.empty.adjustments.description', 'Add discounts, fees, or taxes to refine totals.')}
|
|
496
504
|
actionLabel={t('sales.documents.adjustments.add', 'Add adjustment')}
|
|
497
505
|
onAction={handleOpenCreate}
|
|
506
|
+
disabled={amountsReadOnly}
|
|
498
507
|
/>
|
|
499
508
|
}
|
|
500
509
|
/>
|
|
@@ -17,9 +17,20 @@ type DocumentTotalsProps = {
|
|
|
17
17
|
currency: string | null | undefined
|
|
18
18
|
items: DocumentTotalItem[]
|
|
19
19
|
className?: string
|
|
20
|
+
/** Marks the header as caller-asserted, e.g. "Amounts from source". */
|
|
21
|
+
sourceBadge?: { label: string; hint?: string } | null
|
|
22
|
+
/** Rendered under the table — the explicit action that leaves the source-owned mode. */
|
|
23
|
+
footerAction?: React.ReactNode
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
export function DocumentTotals({
|
|
26
|
+
export function DocumentTotals({
|
|
27
|
+
title,
|
|
28
|
+
currency,
|
|
29
|
+
items,
|
|
30
|
+
className,
|
|
31
|
+
sourceBadge,
|
|
32
|
+
footerAction,
|
|
33
|
+
}: DocumentTotalsProps) {
|
|
23
34
|
const t = useT()
|
|
24
35
|
const emphasizedRows = items.filter((item) => item.emphasize)
|
|
25
36
|
const heading = title ?? t('sales.documents.detail.totals.title')
|
|
@@ -56,7 +67,17 @@ export function DocumentTotals({ title, currency, items, className }: DocumentTo
|
|
|
56
67
|
<div className={cn('space-y-3', className)}>
|
|
57
68
|
<div className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
58
69
|
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-3">
|
|
59
|
-
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
70
|
+
<span className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
71
|
+
{heading}
|
|
72
|
+
{sourceBadge ? (
|
|
73
|
+
<span
|
|
74
|
+
title={sourceBadge.hint}
|
|
75
|
+
className="rounded-full bg-status-info-bg px-2 py-0.5 text-xs font-semibold normal-case tracking-normal text-status-info-text"
|
|
76
|
+
>
|
|
77
|
+
{sourceBadge.label}
|
|
78
|
+
</span>
|
|
79
|
+
) : null}
|
|
80
|
+
</span>
|
|
60
81
|
{currency ? (
|
|
61
82
|
<span className="rounded-full border border-border/70 bg-background/80 px-3 py-1 text-xs font-semibold tracking-wide text-foreground">
|
|
62
83
|
{currency}
|
|
@@ -113,6 +134,9 @@ export function DocumentTotals({ title, currency, items, className }: DocumentTo
|
|
|
113
134
|
</button>
|
|
114
135
|
</div>
|
|
115
136
|
) : null}
|
|
137
|
+
{footerAction ? (
|
|
138
|
+
<div className="flex items-center justify-end border-t bg-muted/30 px-4 py-3">{footerAction}</div>
|
|
139
|
+
) : null}
|
|
116
140
|
</div>
|
|
117
141
|
</div>
|
|
118
142
|
)
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createDictionaryMap,
|
|
17
17
|
normalizeDictionaryEntries,
|
|
18
18
|
} from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
|
|
19
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
19
20
|
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
20
21
|
import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
21
22
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
@@ -151,6 +152,8 @@ type SalesDocumentItemsSectionProps = {
|
|
|
151
152
|
tenantId?: string | null;
|
|
152
153
|
onActionChange?: (action: SectionAction | null) => void;
|
|
153
154
|
onItemsChange?: (items: SalesLineRecord[]) => void;
|
|
155
|
+
/** Disables line writes: on an external order they would all be rejected. */
|
|
156
|
+
amountsReadOnly?: boolean;
|
|
154
157
|
};
|
|
155
158
|
|
|
156
159
|
export function SalesDocumentItemsSection({
|
|
@@ -162,6 +165,7 @@ export function SalesDocumentItemsSection({
|
|
|
162
165
|
tenantId: tenantFromProps,
|
|
163
166
|
onActionChange,
|
|
164
167
|
onItemsChange,
|
|
168
|
+
amountsReadOnly = false,
|
|
165
169
|
}: SalesDocumentItemsSectionProps) {
|
|
166
170
|
const t = useT();
|
|
167
171
|
const runDocumentMutation = useSalesDocumentMutation(kind, documentId);
|
|
@@ -535,15 +539,19 @@ export function SalesDocumentItemsSection({
|
|
|
535
539
|
onActionChange({
|
|
536
540
|
label: t("sales.documents.items.add", "Add item"),
|
|
537
541
|
onClick: openCreate,
|
|
538
|
-
disabled:
|
|
542
|
+
disabled: amountsReadOnly,
|
|
539
543
|
});
|
|
540
544
|
return () => onActionChange(null);
|
|
541
|
-
}, [onActionChange, openCreate, t]);
|
|
545
|
+
}, [amountsReadOnly, onActionChange, openCreate, t]);
|
|
542
546
|
|
|
543
|
-
const handleEdit = React.useCallback(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
+
const handleEdit = React.useCallback(
|
|
548
|
+
(line: SalesLineRecord) => {
|
|
549
|
+
if (amountsReadOnly) return;
|
|
550
|
+
setLineForEdit(line);
|
|
551
|
+
setDialogOpen(true);
|
|
552
|
+
},
|
|
553
|
+
[amountsReadOnly],
|
|
554
|
+
);
|
|
547
555
|
|
|
548
556
|
const resolveVariantInfo = React.useCallback((record: SalesLineRecord) => {
|
|
549
557
|
const meta =
|
|
@@ -728,10 +736,11 @@ export function SalesDocumentItemsSection({
|
|
|
728
736
|
action={{
|
|
729
737
|
label: t("sales.documents.items.add", "Add item"),
|
|
730
738
|
onClick: openCreate,
|
|
739
|
+
disabled: amountsReadOnly,
|
|
731
740
|
}}
|
|
732
741
|
/>
|
|
733
742
|
) : (
|
|
734
|
-
<div className="overflow-
|
|
743
|
+
<div className="relative overflow-x-auto rounded border">
|
|
735
744
|
<table className="w-full text-sm">
|
|
736
745
|
<thead className="bg-muted">
|
|
737
746
|
<tr className="text-left">
|
|
@@ -810,7 +819,10 @@ export function SalesDocumentItemsSection({
|
|
|
810
819
|
return (
|
|
811
820
|
<tr
|
|
812
821
|
key={item.id}
|
|
813
|
-
className=
|
|
822
|
+
className={cn(
|
|
823
|
+
"border-t transition-colors",
|
|
824
|
+
amountsReadOnly ? "" : "cursor-pointer hover:bg-muted/50",
|
|
825
|
+
)}
|
|
814
826
|
onClick={() => handleEdit(item)}
|
|
815
827
|
>
|
|
816
828
|
<td className="px-3 py-3">
|
|
@@ -995,6 +1007,7 @@ export function SalesDocumentItemsSection({
|
|
|
995
1007
|
variant="ghost"
|
|
996
1008
|
className="h-8 w-8"
|
|
997
1009
|
aria-label={t('ui.actions.edit', 'Edit')}
|
|
1010
|
+
disabled={amountsReadOnly}
|
|
998
1011
|
onClick={(event) => {
|
|
999
1012
|
event.stopPropagation();
|
|
1000
1013
|
handleEdit(item);
|
|
@@ -1003,16 +1016,18 @@ export function SalesDocumentItemsSection({
|
|
|
1003
1016
|
<Pencil className="h-4 w-4" />
|
|
1004
1017
|
</Button>
|
|
1005
1018
|
<span
|
|
1006
|
-
title={
|
|
1007
|
-
? t('sales.documents.
|
|
1008
|
-
:
|
|
1019
|
+
title={amountsReadOnly
|
|
1020
|
+
? t('sales.documents.amountsExternalHint', 'Totals on this document come from an external system and are stored as supplied.')
|
|
1021
|
+
: kind === 'order' && items.length === 1
|
|
1022
|
+
? t('sales.documents.items.errorDeleteLast', 'An order must contain at least one line item.')
|
|
1023
|
+
: undefined}
|
|
1009
1024
|
>
|
|
1010
1025
|
<Button
|
|
1011
1026
|
size="icon"
|
|
1012
1027
|
variant="ghost"
|
|
1013
1028
|
className="h-8 w-8 text-destructive"
|
|
1014
1029
|
aria-label={t('ui.actions.delete', 'Delete')}
|
|
1015
|
-
disabled={kind === 'order' && items.length === 1}
|
|
1030
|
+
disabled={amountsReadOnly || (kind === 'order' && items.length === 1)}
|
|
1016
1031
|
onClick={(event) => {
|
|
1017
1032
|
event.stopPropagation();
|
|
1018
1033
|
void handleDelete(item);
|
|
@@ -8,6 +8,7 @@ export type SalesDocumentKind = 'order' | 'quote' | 'invoice' | 'credit_memo'
|
|
|
8
8
|
export type SalesLineKind = 'product' | 'service' | 'shipping' | 'discount' | 'adjustment'
|
|
9
9
|
export const DEFAULT_SALES_ADJUSTMENT_KINDS = ['discount', 'tax', 'shipping', 'surcharge', 'return', 'custom'] as const
|
|
10
10
|
export type SalesAdjustmentKind = (typeof DEFAULT_SALES_ADJUSTMENT_KINDS)[number] | string
|
|
11
|
+
export type SalesAmountsMode = 'computed' | 'external'
|
|
11
12
|
|
|
12
13
|
@Entity({ tableName: 'sales_channels' })
|
|
13
14
|
@Index({ name: 'sales_channels_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
|
|
@@ -331,6 +332,9 @@ export class SalesTaxRate {
|
|
|
331
332
|
@Index({ name: 'sales_orders_payment_status_idx', properties: ['organizationId', 'tenantId', 'paymentStatus'] })
|
|
332
333
|
@Unique({ name: 'sales_orders_number_unique', properties: ['organizationId', 'tenantId', 'orderNumber'] })
|
|
333
334
|
export class SalesOrder {
|
|
335
|
+
// Optional so existing `em.create(SalesOrder, ...)` callers keep compiling.
|
|
336
|
+
[OptionalProps]?: 'totalsMode'
|
|
337
|
+
|
|
334
338
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
335
339
|
id!: string
|
|
336
340
|
|
|
@@ -466,6 +470,10 @@ export class SalesOrder {
|
|
|
466
470
|
@Property({ name: 'outstanding_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })
|
|
467
471
|
outstandingAmount: string = '0'
|
|
468
472
|
|
|
473
|
+
/** `external`: the header totals are the caller's and never rebuilt; payment totals stay core-owned. */
|
|
474
|
+
@Property({ name: 'totals_mode', type: 'text', default: 'computed' })
|
|
475
|
+
totalsMode: SalesAmountsMode = 'computed'
|
|
476
|
+
|
|
469
477
|
@Property({ name: 'line_item_count', type: 'integer', default: 0 })
|
|
470
478
|
lineItemCount: number = 0
|
|
471
479
|
|
|
@@ -553,6 +561,8 @@ export class SalesOrder {
|
|
|
553
561
|
properties: ['organizationId', 'tenantId', 'normalizedUnit', 'normalizedQuantity'],
|
|
554
562
|
})
|
|
555
563
|
export class SalesOrderLine {
|
|
564
|
+
[OptionalProps]?: 'amountsMode'
|
|
565
|
+
|
|
556
566
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
557
567
|
id!: string
|
|
558
568
|
|
|
@@ -649,6 +659,10 @@ export class SalesOrderLine {
|
|
|
649
659
|
@Property({ name: 'total_gross_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })
|
|
650
660
|
totalGrossAmount: string = '0'
|
|
651
661
|
|
|
662
|
+
/** `external`: amounts are the caller's; `discount_amount` is then signed. Matches the order's `totals_mode`. */
|
|
663
|
+
@Property({ name: 'amounts_mode', type: 'text', default: 'computed' })
|
|
664
|
+
amountsMode: SalesAmountsMode = 'computed'
|
|
665
|
+
|
|
652
666
|
@Property({ name: 'configuration', type: 'jsonb', nullable: true })
|
|
653
667
|
configuration?: Record<string, unknown> | null
|
|
654
668
|
|
|
@@ -329,6 +329,8 @@ const lineKindSchema = z.enum(['product', 'service', 'shipping', 'discount', 'ad
|
|
|
329
329
|
|
|
330
330
|
const adjustmentKindSchema = z.string().trim().min(1).max(150)
|
|
331
331
|
|
|
332
|
+
export const amountsModeSchema = z.enum(['computed', 'external'])
|
|
333
|
+
|
|
332
334
|
const linePricingSchema = z.object({
|
|
333
335
|
quantity: decimal({ min: 0, max: MAX_QUANTITY, message: 'Quantity is too large.' }),
|
|
334
336
|
quantityUnit: z.string().trim().max(25).optional(),
|
|
@@ -348,6 +350,9 @@ const linePricingSchema = z.object({
|
|
|
348
350
|
taxAmount: decimal({ min: 0 }).optional(),
|
|
349
351
|
totalNetAmount: decimal({ min: 0 }).optional(),
|
|
350
352
|
totalGrossAmount: decimal({ min: 0 }).optional(),
|
|
353
|
+
// Omitted inherits the document's mode; defaulting to `computed` would make an
|
|
354
|
+
// external order mixed. Order lines only — quote commands reject it.
|
|
355
|
+
amountsMode: amountsModeSchema.optional(),
|
|
351
356
|
})
|
|
352
357
|
|
|
353
358
|
const uomSnapshotSchema = z.object({
|
|
@@ -672,6 +677,10 @@ const orderTotalsSchema = z.object({
|
|
|
672
677
|
lineItemCount: z.coerce.number().int().min(0).optional(),
|
|
673
678
|
})
|
|
674
679
|
|
|
680
|
+
// `lineItemCount` is core's count of the rows it persisted, not one of the amounts
|
|
681
|
+
// a caller owns; asserting it could make a document disagree with its own lines.
|
|
682
|
+
export const orderHeaderTotalsSchema = orderTotalsSchema.omit({ lineItemCount: true })
|
|
683
|
+
|
|
675
684
|
const quoteTotalsSchema = z.object({
|
|
676
685
|
subtotalNetAmount: decimal({ min: 0 }).optional(),
|
|
677
686
|
subtotalGrossAmount: decimal({ min: 0 }).optional(),
|
|
@@ -728,6 +737,7 @@ export const orderCreateSchema = scoped.extend({
|
|
|
728
737
|
.min(1, SALES_ORDER_LINES_REQUIRED_MESSAGE_KEY),
|
|
729
738
|
adjustments: z.array(orderAdjustmentCreateSchema.omit({ organizationId: true, tenantId: true, orderId: true })).optional(),
|
|
730
739
|
tags: z.array(uuid()).optional(),
|
|
740
|
+
totalsMode: amountsModeSchema.optional(),
|
|
731
741
|
...orderTotalsSchema.shape,
|
|
732
742
|
...orderPaymentLedgerShape,
|
|
733
743
|
})
|
|
@@ -497,6 +497,13 @@
|
|
|
497
497
|
"sales.documents.adjustments.title": "Anpassungen",
|
|
498
498
|
"sales.documents.adjustments.untitled": "Ohne Titel",
|
|
499
499
|
"sales.documents.adjustments.updated": "Anpassung aktualisiert.",
|
|
500
|
+
"sales.documents.amountsExternal": "Beträge aus Quellsystem",
|
|
501
|
+
"sales.documents.amountsExternalHint": "Die Summen dieses Belegs stammen aus einem externen System und werden unverändert gespeichert.",
|
|
502
|
+
"sales.documents.amountsExternalMethodNote": "Die Methode wird gespeichert, aber es wird keine Gebühr berechnet — die Summen dieses Auftrags stammen aus dem Quellsystem.",
|
|
503
|
+
"sales.documents.amountsSwitchConfirm": "Die Beträge dieses Auftrags aus seinen Positionen neu berechnen? Die vom Quellsystem gelieferten Summen werden ersetzt und lassen sich von hier aus nicht wiederherstellen.",
|
|
504
|
+
"sales.documents.amountsSwitchDone": "Die Beträge werden jetzt aus den Positionen berechnet.",
|
|
505
|
+
"sales.documents.amountsSwitchFailed": "Umstellung auf berechnete Beträge fehlgeschlagen.",
|
|
506
|
+
"sales.documents.amountsSwitchToComputed": "Auf berechnete Beträge umstellen",
|
|
500
507
|
"sales.documents.create.title": "Verkaufsdokument erstellen",
|
|
501
508
|
"sales.documents.detail.addresses.add": "Adresse hinzufügen",
|
|
502
509
|
"sales.documents.detail.addresses.additional": "Weitere Adressen",
|
|
@@ -1240,6 +1247,11 @@
|
|
|
1240
1247
|
"sales.documents.status.documentPlaceholder": "Neuen Bestell-/Angebotsstatus auswählen",
|
|
1241
1248
|
"sales.documents.status.linePlaceholder": "Positionsstatus auswählen",
|
|
1242
1249
|
"sales.documents.status.sectionTitle": "Statusänderungen",
|
|
1250
|
+
"sales.errors.externalAdjustmentRefused": "Dieser Auftrag führt Beträge aus einem externen System, daher können keine Anpassungen hinzugefügt oder entfernt werden. Stellen Sie ihn zuerst auf berechnete Beträge um.",
|
|
1251
|
+
"sales.errors.externalAmountsIncomplete": "Position {{line}} hat externe Beträge, aber es fehlen: {{fields}}. Eine unvollständige Angabe ist kein Modus.",
|
|
1252
|
+
"sales.errors.externalModeMixed": "Ein Auftrag und alle seine Positionen müssen denselben Betragsmodus verwenden.",
|
|
1253
|
+
"sales.errors.externalModeUnsupportedOnQuote": "Angebote können keinen Betragsmodus führen; sie verwenden immer berechnete Beträge.",
|
|
1254
|
+
"sales.errors.externalTotalsRequired": "Ein Auftrag mit externen Beträgen muss eigene Belegsummen angeben: {{fields}}.",
|
|
1243
1255
|
"sales.errors.invalid_query": "Ungültige Abfrageparameter",
|
|
1244
1256
|
"sales.errors.organization_required": "Organisationskontext ist erforderlich",
|
|
1245
1257
|
"sales.errors.tag_invalid": "Ungültiges Tag-Format.",
|
|
@@ -497,6 +497,13 @@
|
|
|
497
497
|
"sales.documents.adjustments.title": "Adjustments",
|
|
498
498
|
"sales.documents.adjustments.untitled": "Untitled",
|
|
499
499
|
"sales.documents.adjustments.updated": "Adjustment updated.",
|
|
500
|
+
"sales.documents.amountsExternal": "Amounts from source",
|
|
501
|
+
"sales.documents.amountsExternalHint": "Totals on this document come from an external system and are stored as supplied.",
|
|
502
|
+
"sales.documents.amountsExternalMethodNote": "The method is recorded, but no charge is added — this order’s totals come from the source system.",
|
|
503
|
+
"sales.documents.amountsSwitchConfirm": "Recompute this order’s amounts from its lines? The totals supplied by the source system will be replaced and cannot be restored from here.",
|
|
504
|
+
"sales.documents.amountsSwitchDone": "Amounts are now computed from the lines.",
|
|
505
|
+
"sales.documents.amountsSwitchFailed": "Could not switch to computed amounts.",
|
|
506
|
+
"sales.documents.amountsSwitchToComputed": "Switch to computed amounts",
|
|
500
507
|
"sales.documents.create.title": "Create sales document",
|
|
501
508
|
"sales.documents.detail.addresses.add": "Add address",
|
|
502
509
|
"sales.documents.detail.addresses.additional": "Additional addresses",
|
|
@@ -1240,6 +1247,11 @@
|
|
|
1240
1247
|
"sales.documents.status.documentPlaceholder": "Select new order/quote status",
|
|
1241
1248
|
"sales.documents.status.linePlaceholder": "Select item status",
|
|
1242
1249
|
"sales.documents.status.sectionTitle": "Status changes",
|
|
1250
|
+
"sales.errors.externalAdjustmentRefused": "This order carries amounts from an external system, so adjustments cannot be added or removed. Switch it to computed amounts first.",
|
|
1251
|
+
"sales.errors.externalAmountsIncomplete": "Line {{line}} has external amounts but is missing: {{fields}}. Partial specification is not a mode.",
|
|
1252
|
+
"sales.errors.externalModeMixed": "An order and all of its lines must share the same amounts mode.",
|
|
1253
|
+
"sales.errors.externalModeUnsupportedOnQuote": "Quotes cannot carry an amounts mode; they always use computed amounts.",
|
|
1254
|
+
"sales.errors.externalTotalsRequired": "An order with external amounts must supply its own document totals: {{fields}}.",
|
|
1243
1255
|
"sales.errors.invalid_query": "Invalid query parameters",
|
|
1244
1256
|
"sales.errors.organization_required": "Organization context is required",
|
|
1245
1257
|
"sales.errors.tag_invalid": "Invalid tag format.",
|
|
@@ -497,6 +497,13 @@
|
|
|
497
497
|
"sales.documents.adjustments.title": "Ajustes",
|
|
498
498
|
"sales.documents.adjustments.untitled": "Sin título",
|
|
499
499
|
"sales.documents.adjustments.updated": "Ajuste actualizado.",
|
|
500
|
+
"sales.documents.amountsExternal": "Importes del origen",
|
|
501
|
+
"sales.documents.amountsExternalHint": "Los totales de este documento provienen de un sistema externo y se guardan tal cual.",
|
|
502
|
+
"sales.documents.amountsExternalMethodNote": "El método se guarda, pero no añade ningún cargo: los totales de este pedido provienen del sistema de origen.",
|
|
503
|
+
"sales.documents.amountsSwitchConfirm": "¿Recalcular los importes de este pedido a partir de sus líneas? Los totales aportados por el sistema de origen se reemplazarán y no se podrán restaurar desde aquí.",
|
|
504
|
+
"sales.documents.amountsSwitchDone": "Los importes ahora se calculan a partir de las líneas.",
|
|
505
|
+
"sales.documents.amountsSwitchFailed": "No se pudo cambiar a importes calculados.",
|
|
506
|
+
"sales.documents.amountsSwitchToComputed": "Cambiar a importes calculados",
|
|
500
507
|
"sales.documents.create.title": "Crear documento de ventas",
|
|
501
508
|
"sales.documents.detail.addresses.add": "Agregar dirección",
|
|
502
509
|
"sales.documents.detail.addresses.additional": "Direcciones adicionales",
|
|
@@ -1240,6 +1247,11 @@
|
|
|
1240
1247
|
"sales.documents.status.documentPlaceholder": "Seleccionar nuevo estado de pedido/cotización",
|
|
1241
1248
|
"sales.documents.status.linePlaceholder": "Seleccionar estado del artículo",
|
|
1242
1249
|
"sales.documents.status.sectionTitle": "Cambios de estado",
|
|
1250
|
+
"sales.errors.externalAdjustmentRefused": "Este pedido lleva importes de un sistema externo, por lo que no se pueden añadir ni eliminar ajustes. Cámbialo primero a importes calculados.",
|
|
1251
|
+
"sales.errors.externalAmountsIncomplete": "La línea {{line}} tiene importes externos, pero faltan: {{fields}}. Una especificación parcial no es un modo.",
|
|
1252
|
+
"sales.errors.externalModeMixed": "Un pedido y todas sus líneas deben compartir el mismo modo de importes.",
|
|
1253
|
+
"sales.errors.externalModeUnsupportedOnQuote": "Los presupuestos no pueden llevar un modo de importes; siempre usan importes calculados.",
|
|
1254
|
+
"sales.errors.externalTotalsRequired": "Un pedido con importes externos debe aportar sus propios totales de documento: {{fields}}.",
|
|
1243
1255
|
"sales.errors.invalid_query": "Parámetros de consulta inválidos",
|
|
1244
1256
|
"sales.errors.organization_required": "Se requiere el contexto de la organización",
|
|
1245
1257
|
"sales.errors.tag_invalid": "Formato de etiqueta no válido.",
|
|
@@ -497,6 +497,13 @@
|
|
|
497
497
|
"sales.documents.adjustments.title": "조정 항목",
|
|
498
498
|
"sales.documents.adjustments.untitled": "제목 없음",
|
|
499
499
|
"sales.documents.adjustments.updated": "조정 항목이 수정되었습니다.",
|
|
500
|
+
"sales.documents.amountsExternal": "원본 시스템 금액",
|
|
501
|
+
"sales.documents.amountsExternalHint": "이 문서의 합계는 외부 시스템에서 제공된 값 그대로 저장됩니다.",
|
|
502
|
+
"sales.documents.amountsExternalMethodNote": "수단은 저장되지만 요금은 추가되지 않습니다. 이 주문의 합계는 원본 시스템에서 옵니다.",
|
|
503
|
+
"sales.documents.amountsSwitchConfirm": "이 주문의 금액을 품목에서 다시 계산할까요? 원본 시스템이 제공한 합계는 대체되며 여기서는 복원할 수 없습니다.",
|
|
504
|
+
"sales.documents.amountsSwitchDone": "이제 금액이 품목에서 계산됩니다.",
|
|
505
|
+
"sales.documents.amountsSwitchFailed": "계산 금액으로 전환하지 못했습니다.",
|
|
506
|
+
"sales.documents.amountsSwitchToComputed": "계산 금액으로 전환",
|
|
500
507
|
"sales.documents.create.title": "판매 문서 생성",
|
|
501
508
|
"sales.documents.detail.addresses.add": "주소 추가",
|
|
502
509
|
"sales.documents.detail.addresses.additional": "추가 주소",
|
|
@@ -1240,6 +1247,11 @@
|
|
|
1240
1247
|
"sales.documents.status.documentPlaceholder": "새 주문/견적 상태 선택",
|
|
1241
1248
|
"sales.documents.status.linePlaceholder": "항목 상태 선택",
|
|
1242
1249
|
"sales.documents.status.sectionTitle": "상태 변경",
|
|
1250
|
+
"sales.errors.externalAdjustmentRefused": "이 주문의 금액은 외부 시스템에서 온 것이므로 조정을 추가하거나 삭제할 수 없습니다. 먼저 계산 금액으로 전환하세요.",
|
|
1251
|
+
"sales.errors.externalAmountsIncomplete": "{{line}}번 품목에 외부 금액이 있지만 누락된 항목이 있습니다: {{fields}}. 부분 지정은 모드가 아닙니다.",
|
|
1252
|
+
"sales.errors.externalModeMixed": "주문과 모든 품목은 동일한 금액 모드를 사용해야 합니다.",
|
|
1253
|
+
"sales.errors.externalModeUnsupportedOnQuote": "견적에는 금액 모드를 지정할 수 없습니다. 견적은 항상 계산 금액을 사용합니다.",
|
|
1254
|
+
"sales.errors.externalTotalsRequired": "외부 금액을 사용하는 주문은 문서 합계를 직접 제공해야 합니다: {{fields}}.",
|
|
1243
1255
|
"sales.errors.invalid_query": "잘못된 조회 파라미터입니다",
|
|
1244
1256
|
"sales.errors.organization_required": "조직 컨텍스트가 필요합니다",
|
|
1245
1257
|
"sales.errors.tag_invalid": "잘못된 태그 형식입니다.",
|
|
@@ -497,6 +497,13 @@
|
|
|
497
497
|
"sales.documents.adjustments.title": "Korekty",
|
|
498
498
|
"sales.documents.adjustments.untitled": "Bez tytułu",
|
|
499
499
|
"sales.documents.adjustments.updated": "Korekta zaktualizowana.",
|
|
500
|
+
"sales.documents.amountsExternal": "Kwoty ze źródła",
|
|
501
|
+
"sales.documents.amountsExternalHint": "Sumy tego dokumentu pochodzą z systemu zewnętrznego i są zapisane w podanej postaci.",
|
|
502
|
+
"sales.documents.amountsExternalMethodNote": "Metoda zostanie zapisana, ale nie doliczy opłaty — sumy tego zamówienia pochodzą z systemu źródłowego.",
|
|
503
|
+
"sales.documents.amountsSwitchConfirm": "Przeliczyć kwoty tego zamówienia z pozycji? Sumy podane przez system źródłowy zostaną zastąpione i nie da się ich stąd przywrócić.",
|
|
504
|
+
"sales.documents.amountsSwitchDone": "Kwoty są teraz wyliczane z pozycji.",
|
|
505
|
+
"sales.documents.amountsSwitchFailed": "Nie udało się przełączyć na kwoty wyliczane.",
|
|
506
|
+
"sales.documents.amountsSwitchToComputed": "Przełącz na kwoty wyliczane",
|
|
500
507
|
"sales.documents.create.title": "Utwórz dokument sprzedaży",
|
|
501
508
|
"sales.documents.detail.addresses.add": "Dodaj adres",
|
|
502
509
|
"sales.documents.detail.addresses.additional": "Dodatkowe adresy",
|
|
@@ -1240,6 +1247,11 @@
|
|
|
1240
1247
|
"sales.documents.status.documentPlaceholder": "Wybierz nowy status zamówienia/oferty",
|
|
1241
1248
|
"sales.documents.status.linePlaceholder": "Wybierz status pozycji",
|
|
1242
1249
|
"sales.documents.status.sectionTitle": "Zmiany statusu",
|
|
1250
|
+
"sales.errors.externalAdjustmentRefused": "To zamówienie ma kwoty z systemu zewnętrznego, więc nie można dodawać ani usuwać korekt. Najpierw przełącz je na kwoty wyliczane.",
|
|
1251
|
+
"sales.errors.externalAmountsIncomplete": "Pozycja {{line}} ma kwoty zewnętrzne, ale brakuje: {{fields}}. Częściowa specyfikacja nie jest trybem.",
|
|
1252
|
+
"sales.errors.externalModeMixed": "Zamówienie i wszystkie jego pozycje muszą mieć ten sam tryb kwot.",
|
|
1253
|
+
"sales.errors.externalModeUnsupportedOnQuote": "Oferty nie mogą mieć trybu kwot; zawsze używają kwot wyliczanych.",
|
|
1254
|
+
"sales.errors.externalTotalsRequired": "Zamówienie z kwotami zewnętrznymi musi podać własne sumy dokumentu: {{fields}}.",
|
|
1243
1255
|
"sales.errors.invalid_query": "Nieprawidłowe parametry zapytania",
|
|
1244
1256
|
"sales.errors.organization_required": "Wymagany jest kontekst organizacji",
|
|
1245
1257
|
"sales.errors.tag_invalid": "Nieprawidłowy format tagu.",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
2
|
import {
|
|
3
3
|
type SalesAdjustmentDraft,
|
|
4
|
+
type SalesAmountsMode,
|
|
5
|
+
type SalesDocumentAmounts,
|
|
4
6
|
type SalesCalculationContext,
|
|
5
7
|
type CalculateDocumentOptions,
|
|
6
8
|
type CalculateLineOptions,
|
|
@@ -115,7 +117,49 @@ function resolveLineDiscountTotal(
|
|
|
115
117
|
return line.discountAmountBasis === 'line' ? amount : amount * quantity
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/** The header fields a caller owns under `external`; the rest stay core-derived. */
|
|
121
|
+
export const EXTERNAL_HEADER_FIELDS = [
|
|
122
|
+
'subtotalNetAmount',
|
|
123
|
+
'subtotalGrossAmount',
|
|
124
|
+
'discountTotalAmount',
|
|
125
|
+
'taxTotalAmount',
|
|
126
|
+
'shippingNetAmount',
|
|
127
|
+
'shippingGrossAmount',
|
|
128
|
+
'surchargeTotalAmount',
|
|
129
|
+
'grandTotalNetAmount',
|
|
130
|
+
'grandTotalGrossAmount',
|
|
131
|
+
] as const
|
|
132
|
+
|
|
133
|
+
export function isExternalAmountsMode(mode: SalesAmountsMode | null | undefined): boolean {
|
|
134
|
+
return mode === 'external'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveSuppliedTotals(
|
|
138
|
+
supplied: Partial<SalesDocumentAmounts> | null | undefined
|
|
139
|
+
): Pick<SalesDocumentAmounts, (typeof EXTERNAL_HEADER_FIELDS)[number]> {
|
|
140
|
+
const resolved = {} as Record<(typeof EXTERNAL_HEADER_FIELDS)[number], number>
|
|
141
|
+
for (const field of EXTERNAL_HEADER_FIELDS) {
|
|
142
|
+
resolved[field] = round(toNumber(supplied?.[field], 0))
|
|
143
|
+
}
|
|
144
|
+
return resolved
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The derived discount is signed and unclamped: a markup is a negative discount.
|
|
148
|
+
function buildExternalLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {
|
|
149
|
+
const quantity = Math.max(toNumber(line.quantity, 0), 0)
|
|
150
|
+
const netAmount = round(toNumber(line.totalNetAmount, 0))
|
|
151
|
+
return {
|
|
152
|
+
line,
|
|
153
|
+
netAmount,
|
|
154
|
+
grossAmount: round(toNumber(line.totalGrossAmount, 0)),
|
|
155
|
+
taxAmount: round(toNumber(line.taxAmount, 0)),
|
|
156
|
+
discountAmount: round(toNumber(line.unitPriceNet, 0) * quantity - netAmount),
|
|
157
|
+
adjustments: [],
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
118
161
|
function buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {
|
|
162
|
+
if (isExternalAmountsMode(line.amountsMode)) return buildExternalLineResult(line)
|
|
119
163
|
const quantity = Math.max(toNumber(line.quantity, 0), 0)
|
|
120
164
|
const taxRate = toNumber(line.taxRate, 0) / 100
|
|
121
165
|
const unitNet =
|
|
@@ -195,6 +239,8 @@ function buildBaseDocumentResult(params: {
|
|
|
195
239
|
adjustments: SalesAdjustmentDraft[]
|
|
196
240
|
currencyCode: string
|
|
197
241
|
existingTotals?: { paidTotalAmount?: number | null; refundedTotalAmount?: number | null }
|
|
242
|
+
totalsMode?: SalesAmountsMode | null
|
|
243
|
+
suppliedTotals?: Partial<SalesDocumentAmounts> | null
|
|
198
244
|
}): SalesDocumentCalculationResult {
|
|
199
245
|
const { documentKind, lines, adjustments, currencyCode } = params
|
|
200
246
|
const orderedAdjustments = [...(adjustments ?? [])].sort(
|
|
@@ -313,6 +359,25 @@ function buildBaseDocumentResult(params: {
|
|
|
313
359
|
const refundedTotalAmount = Math.max(toNumber(params.existingTotals?.refundedTotalAmount, 0), 0)
|
|
314
360
|
const outstandingAmount = Math.max(grandTotalGross - paidTotalAmount + refundedTotalAmount, 0)
|
|
315
361
|
|
|
362
|
+
if (isExternalAmountsMode(params.totalsMode)) {
|
|
363
|
+
const suppliedTotals = resolveSuppliedTotals(params.suppliedTotals)
|
|
364
|
+
return {
|
|
365
|
+
kind: documentKind,
|
|
366
|
+
currencyCode,
|
|
367
|
+
lines,
|
|
368
|
+
adjustments: resolvedAdjustments,
|
|
369
|
+
metadata: {},
|
|
370
|
+
totals: {
|
|
371
|
+
...suppliedTotals,
|
|
372
|
+
paidTotalAmount,
|
|
373
|
+
refundedTotalAmount,
|
|
374
|
+
outstandingAmount: round(
|
|
375
|
+
Math.max(suppliedTotals.grandTotalGrossAmount - paidTotalAmount + refundedTotalAmount, 0)
|
|
376
|
+
),
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
316
381
|
return {
|
|
317
382
|
kind: documentKind,
|
|
318
383
|
currencyCode,
|
|
@@ -389,11 +454,32 @@ class SalesCalculationRegistry {
|
|
|
389
454
|
})
|
|
390
455
|
}
|
|
391
456
|
|
|
457
|
+
// Re-applied after the registry and events, which may otherwise overwrite it.
|
|
458
|
+
if (isExternalAmountsMode(line.amountsMode)) {
|
|
459
|
+
const supplied = buildExternalLineResult(line)
|
|
460
|
+
current = {
|
|
461
|
+
...current,
|
|
462
|
+
netAmount: supplied.netAmount,
|
|
463
|
+
grossAmount: supplied.grossAmount,
|
|
464
|
+
taxAmount: supplied.taxAmount,
|
|
465
|
+
discountAmount: supplied.discountAmount,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
392
469
|
return current
|
|
393
470
|
}
|
|
394
471
|
|
|
395
472
|
async calculateDocument(opts: CalculateDocumentOptions): Promise<SalesDocumentCalculationResult> {
|
|
396
|
-
const {
|
|
473
|
+
const {
|
|
474
|
+
documentKind,
|
|
475
|
+
lines,
|
|
476
|
+
adjustments = [],
|
|
477
|
+
context,
|
|
478
|
+
eventBus,
|
|
479
|
+
existingTotals,
|
|
480
|
+
totalsMode,
|
|
481
|
+
suppliedTotals,
|
|
482
|
+
} = opts
|
|
397
483
|
const resolvedLines: SalesLineCalculationResult[] = []
|
|
398
484
|
|
|
399
485
|
for (const line of lines) {
|
|
@@ -407,6 +493,8 @@ class SalesCalculationRegistry {
|
|
|
407
493
|
adjustments,
|
|
408
494
|
currencyCode: context.currencyCode,
|
|
409
495
|
existingTotals,
|
|
496
|
+
totalsMode,
|
|
497
|
+
suppliedTotals,
|
|
410
498
|
})
|
|
411
499
|
|
|
412
500
|
if (eventBus) {
|
|
@@ -430,6 +518,7 @@ class SalesCalculationRegistry {
|
|
|
430
518
|
context,
|
|
431
519
|
current,
|
|
432
520
|
eventBus,
|
|
521
|
+
totalsMode,
|
|
433
522
|
})
|
|
434
523
|
if (next) current = next
|
|
435
524
|
}
|
|
@@ -453,9 +542,15 @@ class SalesCalculationRegistry {
|
|
|
453
542
|
// outstanding back to the full grand total), producing a stale paid/
|
|
454
543
|
// outstanding display after a payment. Re-apply the input totals last and
|
|
455
544
|
// recompute outstanding against the post-calculation grand total.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
545
|
+
// Totals calculators rebuild the header from the lines, and core registers one
|
|
546
|
+
// itself by module side effect (lib/providers/index.ts) — so this always runs.
|
|
547
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
548
|
+
current.totals = { ...current.totals, ...resolveSuppliedTotals(suppliedTotals) }
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (existingTotals || isExternalAmountsMode(totalsMode)) {
|
|
552
|
+
const paidTotalAmount = Math.max(toNumber(existingTotals?.paidTotalAmount, 0), 0)
|
|
553
|
+
const refundedTotalAmount = Math.max(toNumber(existingTotals?.refundedTotalAmount, 0), 0)
|
|
459
554
|
current.totals = {
|
|
460
555
|
...current.totals,
|
|
461
556
|
paidTotalAmount,
|