@open-mercato/core 0.6.3-develop.3525.2.cc18b8a43f → 0.6.3-develop.3562.1.71049275ff
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/users/route.js +49 -9
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/backend/users/roleOptions.js +1 -1
- package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +8 -0
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +53 -26
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +104 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +20 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/route.ts +51 -9
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/auth/backend/users/roleOptions.ts +2 -1
- package/src/modules/auth/commands/roles.ts +8 -0
- package/src/modules/auth/components/AclEditor.tsx +50 -24
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
- package/src/modules/sales/commands/documents.ts +116 -0
- package/src/modules/sales/data/validators.ts +38 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
|
@@ -7,6 +7,7 @@ type RoleListResponse = {
|
|
|
7
7
|
|
|
8
8
|
type FetchRoleOptionsParams = {
|
|
9
9
|
tenantId?: string | null
|
|
10
|
+
includeSuperAdmin?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function fetchRoleOptions(query?: string, params?: FetchRoleOptionsParams): Promise<CrudFieldOption[]> {
|
|
@@ -28,7 +29,7 @@ export async function fetchRoleOptions(query?: string, params?: FetchRoleOptions
|
|
|
28
29
|
const id = typeof item?.id === 'string' ? item.id.trim() : ''
|
|
29
30
|
const name = typeof item?.name === 'string' ? item?.name.trim() : ''
|
|
30
31
|
if (!id || !name) return null
|
|
31
|
-
if (name === 'superadmin') return null
|
|
32
|
+
if (name === 'superadmin' && !params?.includeSuperAdmin) return null
|
|
32
33
|
return { value: id, label: name }
|
|
33
34
|
})
|
|
34
35
|
.filter((opt): opt is CrudFieldOption => !!opt)
|
|
@@ -235,6 +235,14 @@ const updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
const wantsTenantChange = parsed.tenantId !== undefined && parsed.tenantId !== current.tenantId
|
|
239
|
+
if (wantsTenantChange) {
|
|
240
|
+
const assignments = await em.count(UserRole, { role: current, deletedAt: null })
|
|
241
|
+
if (assignments > 0) {
|
|
242
|
+
throw new CrudHttpError(400, { error: 'Role cannot be moved to another tenant while users are assigned' })
|
|
243
|
+
}
|
|
244
|
+
await em.nativeDelete(RoleAcl, { role: parsed.id as unknown as Role })
|
|
245
|
+
}
|
|
238
246
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
239
247
|
const role = await de.updateOrmEntity({
|
|
240
248
|
entity: Role,
|
|
@@ -100,6 +100,7 @@ export function AclEditor({
|
|
|
100
100
|
userRoles,
|
|
101
101
|
currentUserIsSuperAdmin,
|
|
102
102
|
tenantId,
|
|
103
|
+
preserveOnTenantChange = false,
|
|
103
104
|
}: {
|
|
104
105
|
kind: 'user' | 'role'
|
|
105
106
|
targetId: string
|
|
@@ -109,6 +110,7 @@ export function AclEditor({
|
|
|
109
110
|
userRoles?: string[]
|
|
110
111
|
currentUserIsSuperAdmin?: boolean
|
|
111
112
|
tenantId?: string | null
|
|
113
|
+
preserveOnTenantChange?: boolean
|
|
112
114
|
}) {
|
|
113
115
|
const actorIsSuperAdmin = !!currentUserIsSuperAdmin
|
|
114
116
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -142,8 +144,33 @@ export function AclEditor({
|
|
|
142
144
|
[actorSanitizeFeatures],
|
|
143
145
|
)
|
|
144
146
|
|
|
147
|
+
const tenantIdRef = React.useRef(tenantId)
|
|
148
|
+
React.useEffect(() => { tenantIdRef.current = tenantId }, [tenantId])
|
|
149
|
+
const hasMountedRef = React.useRef(false)
|
|
150
|
+
|
|
151
|
+
const fetchAclState = React.useCallback(async (forTenantId: string | null | undefined, cancelledRef: { current: boolean }) => {
|
|
152
|
+
try {
|
|
153
|
+
const aclQuery = new URLSearchParams()
|
|
154
|
+
aclQuery.set(kind === 'user' ? 'userId' : 'roleId', targetId)
|
|
155
|
+
if (forTenantId) aclQuery.set('tenantId', forTenantId)
|
|
156
|
+
const aclQueryString = aclQuery.toString()
|
|
157
|
+
const aclJson = await readJsonOr<AclPayload>(
|
|
158
|
+
`/api/auth/${kind === 'user' ? 'users' : 'roles'}/acl${aclQueryString ? `?${aclQueryString}` : ''}`,
|
|
159
|
+
undefined,
|
|
160
|
+
{ hasCustomAcl: true, isSuperAdmin: false, features: [], organizations: null },
|
|
161
|
+
)
|
|
162
|
+
if (cancelledRef.current) return
|
|
163
|
+
const customAclExists = aclJson.hasCustomAcl !== false
|
|
164
|
+
setHasCustomAcl(customAclExists)
|
|
165
|
+
setOverrideEnabled(customAclExists)
|
|
166
|
+
setIsSuperAdmin(!!aclJson.isSuperAdmin)
|
|
167
|
+
setGranted(actorSanitizeFeatures(aclJson.features))
|
|
168
|
+
setOrganizations(aclJson.organizations == null ? null : Array.isArray(aclJson.organizations) ? aclJson.organizations : [])
|
|
169
|
+
} catch {}
|
|
170
|
+
}, [kind, targetId, actorSanitizeFeatures])
|
|
171
|
+
|
|
145
172
|
React.useEffect(() => {
|
|
146
|
-
|
|
173
|
+
const cancelled = { current: false }
|
|
147
174
|
async function load() {
|
|
148
175
|
setLoading(true)
|
|
149
176
|
try {
|
|
@@ -152,30 +179,30 @@ export function AclEditor({
|
|
|
152
179
|
undefined,
|
|
153
180
|
{ items: [], modules: [] },
|
|
154
181
|
)
|
|
155
|
-
if (!cancelled) {
|
|
182
|
+
if (!cancelled.current) {
|
|
156
183
|
setFeatures(fJson.items || [])
|
|
157
184
|
setModules(fJson.modules || [])
|
|
158
185
|
}
|
|
159
186
|
} catch {}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
await fetchAclState(tenantIdRef.current, cancelled)
|
|
188
|
+
hasMountedRef.current = true
|
|
189
|
+
if (!cancelled.current) setLoading(false)
|
|
190
|
+
}
|
|
191
|
+
load()
|
|
192
|
+
return () => { cancelled.current = true }
|
|
193
|
+
}, [kind, targetId, fetchAclState])
|
|
194
|
+
|
|
195
|
+
React.useEffect(() => {
|
|
196
|
+
if (!hasMountedRef.current) return
|
|
197
|
+
if (preserveOnTenantChange) return
|
|
198
|
+
const cancelled = { current: false }
|
|
199
|
+
fetchAclState(tenantId, cancelled)
|
|
200
|
+
return () => { cancelled.current = true }
|
|
201
|
+
}, [tenantId, preserveOnTenantChange, fetchAclState])
|
|
202
|
+
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
let cancelled = false
|
|
205
|
+
async function loadTenantScoped() {
|
|
179
206
|
if (canEditOrganizations) {
|
|
180
207
|
try {
|
|
181
208
|
const orgQuery = new URLSearchParams()
|
|
@@ -206,11 +233,10 @@ export function AclEditor({
|
|
|
206
233
|
}
|
|
207
234
|
} catch {}
|
|
208
235
|
}
|
|
209
|
-
if (!cancelled) setLoading(false)
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
loadTenantScoped()
|
|
212
238
|
return () => { cancelled = true }
|
|
213
|
-
}, [kind,
|
|
239
|
+
}, [kind, canEditOrganizations, userRoles, tenantId])
|
|
214
240
|
|
|
215
241
|
// Notify parent of changes
|
|
216
242
|
React.useEffect(() => {
|
|
@@ -32,6 +32,7 @@ type UserResponse = {
|
|
|
32
32
|
type BaseProps = {
|
|
33
33
|
tenantId?: string | null
|
|
34
34
|
organizationId?: string | null
|
|
35
|
+
preserveOnTenantChange?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
type RoleProps = BaseProps & {
|
|
@@ -92,7 +93,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
92
93
|
(widget: WidgetCatalogItem) => resolveWidgetText(t, widget.id, 'description', widget.description || ''),
|
|
93
94
|
[t],
|
|
94
95
|
)
|
|
95
|
-
const { kind, targetId, tenantId, organizationId } = props
|
|
96
|
+
const { kind, targetId, tenantId, organizationId, preserveOnTenantChange = false } = props
|
|
96
97
|
const [catalog, setCatalog] = React.useState<WidgetCatalogItem[]>([])
|
|
97
98
|
const [loading, setLoading] = React.useState(true)
|
|
98
99
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -136,10 +137,16 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
136
137
|
setCatalog(mapped)
|
|
137
138
|
}, [])
|
|
138
139
|
|
|
139
|
-
const
|
|
140
|
+
const tenantIdRef = React.useRef(tenantId)
|
|
141
|
+
React.useEffect(() => { tenantIdRef.current = tenantId }, [tenantId])
|
|
142
|
+
const organizationIdRef = React.useRef(organizationId)
|
|
143
|
+
React.useEffect(() => { organizationIdRef.current = organizationId }, [organizationId])
|
|
144
|
+
const hasMountedRef = React.useRef(false)
|
|
145
|
+
|
|
146
|
+
const loadRoleData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
140
147
|
const params = new URLSearchParams({ roleId: targetId })
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
148
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
149
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
143
150
|
const data = await readApiResultOrThrow<RoleResponse>(
|
|
144
151
|
`/api/dashboards/roles/widgets?${params.toString()}`,
|
|
145
152
|
undefined,
|
|
@@ -151,12 +158,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
151
158
|
setMode('override')
|
|
152
159
|
setOriginalMode('override')
|
|
153
160
|
setEffective(ids)
|
|
154
|
-
}, [
|
|
161
|
+
}, [targetId])
|
|
155
162
|
|
|
156
|
-
const loadUserData = React.useCallback(async () => {
|
|
163
|
+
const loadUserData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
157
164
|
const params = new URLSearchParams({ userId: targetId })
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
165
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
166
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
160
167
|
const data = await readApiResultOrThrow<UserResponse>(
|
|
161
168
|
`/api/dashboards/users/widgets?${params.toString()}`,
|
|
162
169
|
undefined,
|
|
@@ -168,7 +175,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
168
175
|
setMode(data.mode || 'inherit')
|
|
169
176
|
setOriginalMode(data.mode || 'inherit')
|
|
170
177
|
setEffective(Array.isArray(data.effectiveWidgetIds) ? data.effectiveWidgetIds : [])
|
|
171
|
-
}, [
|
|
178
|
+
}, [targetId])
|
|
172
179
|
|
|
173
180
|
React.useEffect(() => {
|
|
174
181
|
let cancelled = false
|
|
@@ -177,21 +184,40 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
177
184
|
setError(null)
|
|
178
185
|
try {
|
|
179
186
|
await loadCatalog()
|
|
180
|
-
if (kind === 'role') await loadRoleData()
|
|
181
|
-
else await loadUserData()
|
|
187
|
+
if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
|
|
188
|
+
else await loadUserData(tenantIdRef.current, organizationIdRef.current)
|
|
182
189
|
} catch (err) {
|
|
183
190
|
console.error('Failed to load widget visibility data', err)
|
|
184
191
|
if (!cancelled) {
|
|
185
192
|
setError(tRef.current('dashboards.widgets.error.load', 'Unable to load widget configuration.'))
|
|
186
193
|
}
|
|
187
194
|
} finally {
|
|
188
|
-
if (!cancelled)
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setLoading(false)
|
|
197
|
+
hasMountedRef.current = true
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
load()
|
|
192
202
|
return () => { cancelled = true }
|
|
193
203
|
}, [kind, loadCatalog, loadRoleData, loadUserData])
|
|
194
204
|
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
if (!hasMountedRef.current) return
|
|
207
|
+
if (preserveOnTenantChange) return
|
|
208
|
+
let cancelled = false
|
|
209
|
+
async function refetch() {
|
|
210
|
+
try {
|
|
211
|
+
if (kind === 'role') await loadRoleData(tenantId, organizationId)
|
|
212
|
+
else await loadUserData(tenantId, organizationId)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('Failed to reload widget visibility data', err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
refetch()
|
|
218
|
+
return () => { cancelled = true }
|
|
219
|
+
}, [tenantId, organizationId, kind, loadRoleData, loadUserData, preserveOnTenantChange])
|
|
220
|
+
|
|
195
221
|
const toggle = React.useCallback((id: string) => {
|
|
196
222
|
setSelected((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))
|
|
197
223
|
}, [])
|
|
@@ -67,6 +67,9 @@ import {
|
|
|
67
67
|
} from "../../customers/data/entities";
|
|
68
68
|
import {
|
|
69
69
|
enforceAdjustmentSign,
|
|
70
|
+
validateReturnAdjustmentWithinRemaining,
|
|
71
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
72
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
70
73
|
quoteCreateSchema,
|
|
71
74
|
quoteLineCreateSchema,
|
|
72
75
|
quoteAdjustmentCreateSchema,
|
|
@@ -7384,6 +7387,63 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
7384
7387
|
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
7385
7388
|
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
7386
7389
|
});
|
|
7390
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
7391
|
+
(adj) => adj.id === adjustmentId,
|
|
7392
|
+
);
|
|
7393
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
7394
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
7395
|
+
(adj) => adj.id !== adjustmentId,
|
|
7396
|
+
);
|
|
7397
|
+
const baselineCalculation =
|
|
7398
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
7399
|
+
documentKind: "order",
|
|
7400
|
+
lines: calcLines,
|
|
7401
|
+
adjustments: baselineAdjustments,
|
|
7402
|
+
context: calculationContext,
|
|
7403
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
7404
|
+
});
|
|
7405
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
7406
|
+
kind: "return",
|
|
7407
|
+
amountNet:
|
|
7408
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
7409
|
+
amountGross:
|
|
7410
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
7411
|
+
remainingNet: Number(
|
|
7412
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
7413
|
+
),
|
|
7414
|
+
remainingGross: Number(
|
|
7415
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
7416
|
+
),
|
|
7417
|
+
});
|
|
7418
|
+
if (issues.length > 0) {
|
|
7419
|
+
const { translate } = await resolveTranslations();
|
|
7420
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
7421
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
7422
|
+
const fieldErrors: Record<string, string> = {};
|
|
7423
|
+
if (grossIssue) {
|
|
7424
|
+
fieldErrors.amountGross = translate(
|
|
7425
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7426
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7427
|
+
);
|
|
7428
|
+
}
|
|
7429
|
+
if (netIssue) {
|
|
7430
|
+
fieldErrors.amountNet = translate(
|
|
7431
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
7432
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
7433
|
+
);
|
|
7434
|
+
}
|
|
7435
|
+
throw new CrudHttpError(400, {
|
|
7436
|
+
error:
|
|
7437
|
+
fieldErrors.amountGross ??
|
|
7438
|
+
fieldErrors.amountNet ??
|
|
7439
|
+
translate(
|
|
7440
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7441
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7442
|
+
),
|
|
7443
|
+
fieldErrors,
|
|
7444
|
+
});
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7387
7447
|
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
7388
7448
|
documentKind: "order",
|
|
7389
7449
|
lines: calcLines,
|
|
@@ -7766,6 +7826,62 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
|
|
|
7766
7826
|
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
7767
7827
|
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
7768
7828
|
});
|
|
7829
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
7830
|
+
(adj) => adj.id === adjustmentId,
|
|
7831
|
+
);
|
|
7832
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
7833
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
7834
|
+
(adj) => adj.id !== adjustmentId,
|
|
7835
|
+
);
|
|
7836
|
+
const baselineCalculation =
|
|
7837
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
7838
|
+
documentKind: "quote",
|
|
7839
|
+
lines: calcLines,
|
|
7840
|
+
adjustments: baselineAdjustments,
|
|
7841
|
+
context: calculationContext,
|
|
7842
|
+
});
|
|
7843
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
7844
|
+
kind: "return",
|
|
7845
|
+
amountNet:
|
|
7846
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
7847
|
+
amountGross:
|
|
7848
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
7849
|
+
remainingNet: Number(
|
|
7850
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
7851
|
+
),
|
|
7852
|
+
remainingGross: Number(
|
|
7853
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
7854
|
+
),
|
|
7855
|
+
});
|
|
7856
|
+
if (issues.length > 0) {
|
|
7857
|
+
const { translate } = await resolveTranslations();
|
|
7858
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
7859
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
7860
|
+
const fieldErrors: Record<string, string> = {};
|
|
7861
|
+
if (grossIssue) {
|
|
7862
|
+
fieldErrors.amountGross = translate(
|
|
7863
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7864
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7865
|
+
);
|
|
7866
|
+
}
|
|
7867
|
+
if (netIssue) {
|
|
7868
|
+
fieldErrors.amountNet = translate(
|
|
7869
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
7870
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
7871
|
+
);
|
|
7872
|
+
}
|
|
7873
|
+
throw new CrudHttpError(400, {
|
|
7874
|
+
error:
|
|
7875
|
+
fieldErrors.amountGross ??
|
|
7876
|
+
fieldErrors.amountNet ??
|
|
7877
|
+
translate(
|
|
7878
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7879
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7880
|
+
),
|
|
7881
|
+
fieldErrors,
|
|
7882
|
+
});
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7769
7885
|
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
7770
7886
|
documentKind: "quote",
|
|
7771
7887
|
lines: calcLines,
|
|
@@ -523,6 +523,44 @@ export const enforceAdjustmentSign = (
|
|
|
523
523
|
*/
|
|
524
524
|
export const enforceReturnAdjustmentSign = enforceAdjustmentSign
|
|
525
525
|
|
|
526
|
+
export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE =
|
|
527
|
+
'Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
|
|
528
|
+
export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE =
|
|
529
|
+
'Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
|
|
530
|
+
|
|
531
|
+
export type ReturnAdjustmentRemainingCheck = {
|
|
532
|
+
kind?: string | null
|
|
533
|
+
amountNet?: number | null
|
|
534
|
+
amountGross?: number | null
|
|
535
|
+
remainingNet: number
|
|
536
|
+
remainingGross: number
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export type ReturnAdjustmentRemainingIssue = {
|
|
540
|
+
path: 'amountNet' | 'amountGross'
|
|
541
|
+
message: string
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Inclusive: abs(amount) === remaining is allowed. Tiny epsilon absorbs
|
|
545
|
+
// floating-point rounding from upstream tax/rate adjustments.
|
|
546
|
+
const RETURN_REMAINING_EPSILON = 0.005
|
|
547
|
+
|
|
548
|
+
export const validateReturnAdjustmentWithinRemaining = (
|
|
549
|
+
value: ReturnAdjustmentRemainingCheck
|
|
550
|
+
): ReturnAdjustmentRemainingIssue[] => {
|
|
551
|
+
if (value.kind !== 'return') return []
|
|
552
|
+
const absNet = typeof value.amountNet === 'number' ? Math.abs(value.amountNet) : 0
|
|
553
|
+
const absGross = typeof value.amountGross === 'number' ? Math.abs(value.amountGross) : 0
|
|
554
|
+
const issues: ReturnAdjustmentRemainingIssue[] = []
|
|
555
|
+
if (absGross > value.remainingGross + RETURN_REMAINING_EPSILON) {
|
|
556
|
+
issues.push({ path: 'amountGross', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE })
|
|
557
|
+
}
|
|
558
|
+
if (absNet > value.remainingNet + RETURN_REMAINING_EPSILON) {
|
|
559
|
+
issues.push({ path: 'amountNet', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE })
|
|
560
|
+
}
|
|
561
|
+
return issues
|
|
562
|
+
}
|
|
563
|
+
|
|
526
564
|
export const orderAdjustmentCreateSchema = scoped.extend({
|
|
527
565
|
orderId: uuid(),
|
|
528
566
|
orderLineId: uuid().optional(),
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Zahlung",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Versand",
|
|
6
6
|
"customers~sales.nav.group": "Vertrieb",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Der Bruttobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Der Nettobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
|
|
7
9
|
"sales.audit.channels.create": "Vertriebskanal erstellen",
|
|
8
10
|
"sales.audit.channels.delete": "Vertriebskanal löschen",
|
|
9
11
|
"sales.audit.channels.update": "Vertriebskanal aktualisieren",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Payment",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Shipment",
|
|
6
6
|
"customers~sales.nav.group": "Sales",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
|
|
7
9
|
"sales.audit.channels.create": "Create sales channel",
|
|
8
10
|
"sales.audit.channels.delete": "Delete sales channel",
|
|
9
11
|
"sales.audit.channels.update": "Update sales channel",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Pago",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Envío",
|
|
6
6
|
"customers~sales.nav.group": "Ventas",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "El importe bruto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "El importe neto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
|
|
7
9
|
"sales.audit.channels.create": "Crear canal de ventas",
|
|
8
10
|
"sales.audit.channels.delete": "Eliminar canal de ventas",
|
|
9
11
|
"sales.audit.channels.update": "Actualizar canal de ventas",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Płatność",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Wysyłka",
|
|
6
6
|
"customers~sales.nav.group": "Sprzedaż",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Kwota brutto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Kwota netto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
|
|
7
9
|
"sales.audit.channels.create": "Utwórz kanał sprzedaży",
|
|
8
10
|
"sales.audit.channels.delete": "Usuń kanał sprzedaży",
|
|
9
11
|
"sales.audit.channels.update": "Zaktualizuj kanał sprzedaży",
|