@things-factory/operato-hub 4.3.697 → 4.3.698
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/config.development.js +1 -1
- package/dist-server/routers/api/restful-apis/v1/utils/params.js +55 -22
- package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-inventory-adjustment.js +184 -79
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-inventory-adjustment.js.map +1 -1
- package/openapi/v1/inventory.yaml +83 -6
- package/openapi/v1/return-order.yaml +27 -0
- package/package.json +2 -2
- package/server/routers/api/restful-apis/v1/utils/params.ts +55 -22
- package/server/routers/api/restful-apis/v1/warehouse/add-inventory-adjustment.ts +278 -85
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
1
|
import { EntityManager, SelectQueryBuilder, getConnection } from 'typeorm'
|
|
3
2
|
|
|
4
3
|
import { restfulApiRouter as router } from '@things-factory/api'
|
|
@@ -9,8 +8,9 @@ import {
|
|
|
9
8
|
InventoryChange,
|
|
10
9
|
submitInventoryChangesService
|
|
11
10
|
} from '@things-factory/warehouse-base'
|
|
12
|
-
import { ProductDetail } from '@things-factory/product-base'
|
|
13
|
-
import { getCompanyBizplace } from '@things-factory/biz-base'
|
|
11
|
+
import { Product, ProductDetail } from '@things-factory/product-base'
|
|
12
|
+
import { Bizplace, getCompanyBizplace } from '@things-factory/biz-base'
|
|
13
|
+
import { Setting } from '@things-factory/setting-base'
|
|
14
14
|
|
|
15
15
|
import { businessMiddleware, loggingMiddleware, validationMiddleware } from '../middlewares'
|
|
16
16
|
import { ApiError, ApiErrorHandler, throwInternalServerError } from '../utils/error-util'
|
|
@@ -37,10 +37,40 @@ type TargetSelector = {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
type AdjustmentInput = {
|
|
40
|
-
target
|
|
41
|
-
changes
|
|
40
|
+
target?: TargetSelector
|
|
41
|
+
changes?: Record<string, any>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type CreateInput = {
|
|
45
|
+
bizplaceId?: string
|
|
46
|
+
locationId?: string
|
|
47
|
+
locationName?: string
|
|
48
|
+
sku?: string
|
|
49
|
+
refCode?: string
|
|
50
|
+
productName?: string
|
|
51
|
+
packingType?: string
|
|
52
|
+
packingSize?: number
|
|
53
|
+
uom?: string
|
|
54
|
+
uomValue?: number
|
|
55
|
+
qty?: number
|
|
56
|
+
unitCost?: number
|
|
57
|
+
batchId?: string
|
|
58
|
+
batchIdRef?: string
|
|
59
|
+
cartonId?: string
|
|
60
|
+
expirationDate?: string
|
|
61
|
+
manufactureDate?: string
|
|
62
|
+
serialNumbers?: string | string[]
|
|
63
|
+
remark?: string
|
|
64
|
+
adjustmentCode?: string
|
|
65
|
+
adjustmentNote?: string
|
|
66
|
+
adjustment_code?: string
|
|
67
|
+
adjustment_note?: string
|
|
42
68
|
}
|
|
43
69
|
|
|
70
|
+
type NormalizedEntry =
|
|
71
|
+
| { kind: 'create'; payload: CreateInput; source: string; index: number }
|
|
72
|
+
| { kind: 'adjust'; payload: AdjustmentInput; source: string; index: number }
|
|
73
|
+
|
|
44
74
|
router.post(
|
|
45
75
|
'/v1/warehouse/add-inventory-adjustment',
|
|
46
76
|
businessMiddleware,
|
|
@@ -51,95 +81,230 @@ router.post(
|
|
|
51
81
|
const { domain, user } = context.state
|
|
52
82
|
const payload = context.request.body?.data
|
|
53
83
|
|
|
54
|
-
if (!payload) throw new ApiError('E01', 'data')
|
|
55
|
-
|
|
56
|
-
// Accept either { adjustments: [...] } or a single adjustment object for convenience
|
|
57
|
-
const adjustments: AdjustmentInput[] = Array.isArray(payload?.adjustments)
|
|
58
|
-
? payload.adjustments
|
|
59
|
-
: Array.isArray(payload)
|
|
60
|
-
? payload
|
|
61
|
-
: payload?.target && payload?.changes
|
|
62
|
-
? [payload as AdjustmentInput]
|
|
63
|
-
: []
|
|
84
|
+
if (!payload) throw new ApiError('E01', 'Request body must contain data')
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const entries: NormalizedEntry[] = normalizeAdjustmentEntries(payload)
|
|
87
|
+
if (!entries.length)
|
|
88
|
+
throw new ApiError(
|
|
89
|
+
'E01',
|
|
90
|
+
'No valid adjustment entries found. Provide create or adjustments array with valid items'
|
|
91
|
+
)
|
|
68
92
|
|
|
69
93
|
// Build patches with deterministic targeting (resolve inventory by pallet/carton/batch/location)
|
|
70
94
|
await getConnection().transaction(async (tx: EntityManager) => {
|
|
71
95
|
const patches: InventoryPatch[] = []
|
|
72
96
|
const correlationIds: string[] = []
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
let strictProduct: boolean = false
|
|
99
|
+
|
|
100
|
+
const strictProductSelectionSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
101
|
+
where: { domain, category: 'id-rule', name: 'strict-product-selection' }
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (strictProductSelectionSetting) strictProduct = strictProductSelectionSetting.value
|
|
105
|
+
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const indexLabel = `${entry.source}[${entry.index}]`
|
|
108
|
+
const correlationId = `rest-adjustment:${uuidv4()}`
|
|
109
|
+
|
|
110
|
+
let rawPatch: any
|
|
111
|
+
let patch: InventoryPatch
|
|
112
|
+
let inventory: Inventory | null = null
|
|
113
|
+
let target: TargetSelector | null = null
|
|
114
|
+
|
|
115
|
+
let rawSerialNumbers: string | string[] | undefined
|
|
116
|
+
let resolvedProductDetail: ProductDetail | null = null
|
|
117
|
+
|
|
118
|
+
if (entry.kind === 'create') {
|
|
119
|
+
rawPatch = entry.payload || {}
|
|
120
|
+
rawSerialNumbers = rawPatch?.serialNumbers
|
|
121
|
+
patch = massageChanges({ ...rawPatch })
|
|
122
|
+
|
|
123
|
+
const sku = rawPatch?.sku
|
|
124
|
+
const refCode: string | undefined = rawPatch?.refCode
|
|
125
|
+
const packingType: string | undefined = rawPatch?.packingType
|
|
126
|
+
const packingSize: number | undefined = rawPatch?.packingSize
|
|
127
|
+
|
|
128
|
+
if (!refCode) {
|
|
129
|
+
throw new ApiError('E01', `refCode is required at ${indexLabel}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const bizplaceId =
|
|
133
|
+
rawPatch?.bizplaceId ||
|
|
134
|
+
rawPatch?.bizplace?.id ||
|
|
135
|
+
(patch?.bizplace as any)?.id ||
|
|
136
|
+
entry.payload?.bizplaceId
|
|
137
|
+
|
|
138
|
+
if (!bizplaceId) {
|
|
139
|
+
throw new ApiError('E01', `bizplaceId is required at ${indexLabel}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const companyBizplace: Bizplace = await getCompanyBizplace(domain, null, bizplaceId, tx)
|
|
143
|
+
const companyDomainId = (companyBizplace?.domain as any)?.id
|
|
144
|
+
|
|
145
|
+
const productDetail: ProductDetail | undefined = await tx
|
|
146
|
+
.getRepository(ProductDetail)
|
|
147
|
+
.createQueryBuilder('pd')
|
|
148
|
+
.innerJoinAndSelect('pd.product', 'product')
|
|
149
|
+
.where('pd.domain = :domain', { domain: companyDomainId })
|
|
150
|
+
.andWhere('pd.deleted_at ISNULL')
|
|
151
|
+
.andWhere('pd.ref_code = :refCode', { refCode })
|
|
152
|
+
.getOne()
|
|
153
|
+
|
|
154
|
+
if (!productDetail) {
|
|
155
|
+
throw new ApiError('E04', 'productDetail not found (invalid sku, refCode, packingType, or packingSize)')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strict product validation: ensure provided packingType and packingSize match productDetail
|
|
159
|
+
if (strictProduct) {
|
|
160
|
+
if (packingType && packingType !== productDetail.packingType) {
|
|
161
|
+
throw new ApiError(
|
|
162
|
+
'E04',
|
|
163
|
+
`packingType mismatch: provided '${packingType}' but productDetail has '${productDetail.packingType}' at ${indexLabel}`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
if (packingSize && packingSize !== productDetail.packingSize) {
|
|
167
|
+
throw new ApiError(
|
|
168
|
+
'E04',
|
|
169
|
+
`packingSize mismatch: provided '${packingSize}' but productDetail has '${productDetail.packingSize}' at ${indexLabel}`
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
patch.productDetailId = productDetail.id
|
|
175
|
+
resolvedProductDetail = productDetail
|
|
176
|
+
if (!patch.sku && productDetail?.product?.sku) {
|
|
177
|
+
patch.sku = productDetail.product.sku
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Assign packingType and packingSize from productDetail if not provided
|
|
181
|
+
if (!packingType && productDetail.packingType) {
|
|
182
|
+
patch.packingType = productDetail.packingType
|
|
183
|
+
}
|
|
184
|
+
if (!packingSize && productDetail.packingSize) {
|
|
185
|
+
patch.packingSize = productDetail.packingSize
|
|
186
|
+
}
|
|
187
|
+
} else if (entry.kind === 'adjust') {
|
|
188
|
+
target = entry.payload?.target || {}
|
|
189
|
+
rawPatch = entry?.payload.changes && typeof entry.payload.changes === 'object' ? entry.payload.changes : {}
|
|
190
|
+
rawSerialNumbers = rawPatch?.serialNumbers
|
|
191
|
+
|
|
192
|
+
// Resolve inventory by target selector. Require at least one selector.
|
|
193
|
+
if (!target?.palletId && !target?.cartonId && !target?.batchId && !target?.locationName) {
|
|
194
|
+
throw new ApiError('E01', `target (need palletId/cartonId/batchId/locationName) at ${indexLabel}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
inventory = await findSingleInventoryByTarget(tx, domain.id, target)
|
|
198
|
+
|
|
199
|
+
// Map human-friendly keys to InventoryPatch structure
|
|
200
|
+
patch = massageChanges(rawPatch)
|
|
201
|
+
|
|
202
|
+
// Attach the resolved inventory identifiers so the mutation updates existing record
|
|
203
|
+
patch.id = inventory.id
|
|
204
|
+
patch.palletId = inventory.palletId
|
|
205
|
+
} else {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
77
208
|
|
|
78
209
|
// Validate required fields
|
|
79
|
-
const adjustmentCode: string | undefined =
|
|
80
|
-
const adjustmentNote: string | undefined =
|
|
210
|
+
const adjustmentCode: string | undefined = rawPatch?.adjustmentCode || rawPatch?.adjustment_code
|
|
211
|
+
const adjustmentNote: string | undefined = rawPatch?.adjustmentNote || rawPatch?.adjustment_note
|
|
81
212
|
|
|
82
213
|
if (!adjustmentCode) {
|
|
83
|
-
throw new ApiError('E01', `adjustmentCode (
|
|
214
|
+
throw new ApiError('E01', `adjustmentCode (required) at ${indexLabel}`)
|
|
84
215
|
}
|
|
85
216
|
if (!adjustmentNote) {
|
|
86
|
-
throw new ApiError('E01', `adjustmentNote (
|
|
217
|
+
throw new ApiError('E01', `adjustmentNote (required) at ${indexLabel}`)
|
|
87
218
|
}
|
|
88
219
|
if (!ALLOWED_ADJUSTMENT_CODES.includes(adjustmentCode)) {
|
|
89
|
-
throw new ApiError('E01', `Invalid adjustmentCode '${adjustmentCode}' at
|
|
220
|
+
throw new ApiError('E01', `Invalid adjustmentCode '${adjustmentCode}' at ${indexLabel}`)
|
|
90
221
|
}
|
|
91
222
|
|
|
92
|
-
// Resolve inventory by target selector. Require at least one selector.
|
|
93
|
-
if (!target?.palletId && !target?.cartonId && !target?.batchId && !target?.locationName) {
|
|
94
|
-
throw new ApiError('E01', `target (need palletId/cartonId/batchId/locationName) at index ${index}`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const inventory: Inventory = await findSingleInventoryByTarget(tx, domain.id, target)
|
|
98
|
-
|
|
99
|
-
// Map human-friendly keys to InventoryPatch structure
|
|
100
|
-
const changes: InventoryPatch = massageChanges(rawChanges)
|
|
101
|
-
|
|
102
|
-
// Attach the resolved inventory identifiers so the mutation updates existing record
|
|
103
|
-
changes.id = inventory.id
|
|
104
|
-
changes.palletId = inventory.palletId
|
|
105
|
-
|
|
106
223
|
// Enforce required note/code
|
|
107
|
-
|
|
108
|
-
|
|
224
|
+
patch.adjustmentCode = adjustmentCode
|
|
225
|
+
patch.adjustmentNote = adjustmentNote
|
|
109
226
|
|
|
110
227
|
// Correlate each generated InventoryChange using details field
|
|
111
|
-
|
|
112
|
-
;(changes as any).details = correlationId
|
|
228
|
+
;(patch as any).details = correlationId
|
|
113
229
|
correlationIds.push(correlationId)
|
|
114
230
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
231
|
+
let serialProduct: Product | null = null
|
|
232
|
+
|
|
233
|
+
if (inventory && target) {
|
|
234
|
+
// If sku is changed, resolve productDetailId using sku, refCode, packingType, and packingSize
|
|
235
|
+
if (patch?.sku && patch.sku !== inventory?.product?.sku) {
|
|
236
|
+
const refCode: string | undefined = rawPatch?.refCode
|
|
237
|
+
const packingType: string | undefined = rawPatch?.packingType
|
|
238
|
+
const packingSize: number | undefined = rawPatch?.packingSize
|
|
239
|
+
|
|
240
|
+
if (!refCode) {
|
|
241
|
+
throw new ApiError('E01', 'adjustment.changes.refCode (required when changing sku)')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const companyBizplace: any = await getCompanyBizplace(
|
|
245
|
+
domain,
|
|
246
|
+
null,
|
|
247
|
+
target.bizplaceId || inventory?.bizplace?.id,
|
|
248
|
+
tx
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const companyDomainId = (companyBizplace?.domain as any)?.id
|
|
252
|
+
|
|
253
|
+
const pd: ProductDetail | undefined = await tx
|
|
254
|
+
.getRepository(ProductDetail)
|
|
255
|
+
.createQueryBuilder('pd')
|
|
256
|
+
.innerJoinAndSelect('pd.product', 'prod')
|
|
257
|
+
.where('pd.domain = :domain', { domain: companyDomainId })
|
|
258
|
+
.andWhere('prod.sku = :sku', { sku: patch.sku })
|
|
259
|
+
.andWhere('pd.deleted_at ISNULL')
|
|
260
|
+
.andWhere('pd.ref_code = :refCode', { refCode })
|
|
261
|
+
.getOne()
|
|
262
|
+
|
|
263
|
+
if (!pd) {
|
|
264
|
+
throw new ApiError('E04', 'productDetail not found (by sku, refCode, packingType, and packingSize)')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Strict product validation for adjustments: ensure provided packingType and packingSize match productDetail
|
|
268
|
+
if (strictProduct) {
|
|
269
|
+
if (packingType && packingType !== pd.packingType) {
|
|
270
|
+
throw new ApiError(
|
|
271
|
+
'E04',
|
|
272
|
+
`packingType mismatch in adjustment: provided '${packingType}' but productDetail has '${pd.packingType}' at ${indexLabel}`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
if (packingSize && packingSize !== pd.packingSize) {
|
|
276
|
+
throw new ApiError(
|
|
277
|
+
'E04',
|
|
278
|
+
`packingSize mismatch in adjustment: provided '${packingSize}' but productDetail has '${pd.packingSize}' at ${indexLabel}`
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (strictProduct) {
|
|
284
|
+
patch.packingType = pd.packingType
|
|
285
|
+
patch.packingSize = pd.packingSize
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
;(patch as any).productDetailId = (pd as any).id
|
|
289
|
+
resolvedProductDetail = pd as ProductDetail
|
|
120
290
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const domainId = (companyBizplace?.domain as any)?.id
|
|
128
|
-
const pd = await tx
|
|
129
|
-
.getRepository(ProductDetail)
|
|
130
|
-
.createQueryBuilder('pd')
|
|
131
|
-
.innerJoinAndSelect('pd.product', 'prod')
|
|
132
|
-
.where('pd.domain = :domain', { domain: domainId })
|
|
133
|
-
.andWhere('prod.sku = :sku', { sku: changes.sku })
|
|
134
|
-
.andWhere('pd.refCode = :refCode', { refCode })
|
|
135
|
-
.getOne()
|
|
136
|
-
if (!pd) {
|
|
137
|
-
throw new ApiError('E04', 'productDetail (by sku and refCode)')
|
|
138
|
-
}
|
|
139
|
-
;(changes as any).productDetailId = (pd as any).id
|
|
291
|
+
|
|
292
|
+
serialProduct = (resolvedProductDetail || inventory.product) as Product
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!serialProduct && resolvedProductDetail) {
|
|
296
|
+
serialProduct = resolvedProductDetail?.product || serialProduct
|
|
140
297
|
}
|
|
141
298
|
|
|
142
|
-
|
|
299
|
+
const needsSerialNumbers = productRequiresSerialTracking(serialProduct)
|
|
300
|
+
if (needsSerialNumbers && !defined(rawSerialNumbers)) {
|
|
301
|
+
throw new ApiError('E01', `serialNumbers (required for serial-tracked product) at ${indexLabel}`)
|
|
302
|
+
}
|
|
303
|
+
if (defined(rawSerialNumbers)) {
|
|
304
|
+
patch.serialNumbers = normalizeSerialNumbersValue(rawSerialNumbers)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
patches.push(patch)
|
|
143
308
|
}
|
|
144
309
|
|
|
145
310
|
// Submit via shared service to preserve core business logic
|
|
@@ -185,6 +350,30 @@ router.post(
|
|
|
185
350
|
}
|
|
186
351
|
)
|
|
187
352
|
|
|
353
|
+
function normalizeAdjustmentEntries(payload: any): NormalizedEntry[] {
|
|
354
|
+
const entries: NormalizedEntry[] = []
|
|
355
|
+
|
|
356
|
+
if (payload?.create && !Array.isArray(payload.create)) {
|
|
357
|
+
throw new ApiError('E01', 'create must be an array')
|
|
358
|
+
}
|
|
359
|
+
if (payload?.adjustments && !Array.isArray(payload.adjustments)) {
|
|
360
|
+
throw new ApiError('E01', 'adjustments must be an array')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const sources = [
|
|
364
|
+
{ data: payload?.create || [], kind: 'create', source: 'create' },
|
|
365
|
+
{ data: payload?.adjustments || [], kind: 'adjust', source: 'adjustments' }
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
entries.push(
|
|
369
|
+
...sources.flatMap(({ data, kind, source }) =>
|
|
370
|
+
data.map((item, idx) => item && { kind, payload: item, source, index: idx }).filter(Boolean)
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return entries
|
|
375
|
+
}
|
|
376
|
+
|
|
188
377
|
async function findSingleInventoryByTarget(
|
|
189
378
|
tx: EntityManager,
|
|
190
379
|
domainId: string,
|
|
@@ -227,33 +416,19 @@ async function findSingleInventoryByTarget(
|
|
|
227
416
|
}
|
|
228
417
|
|
|
229
418
|
function massageChanges(input: any): InventoryPatch {
|
|
230
|
-
// Normalize incoming keys (support snake_case & alternate names)
|
|
231
419
|
const changes = { ...input }
|
|
232
420
|
|
|
233
|
-
// Field aliases from requirement list
|
|
234
|
-
if (changes.batchNo && !changes.batchId) changes.batchId = changes.batchNo
|
|
235
|
-
if (changes.batchNoRef && !changes.batchIdRef) changes.batchIdRef = changes.batchNoRef
|
|
236
|
-
if (changes.company && !changes.bizplaceId) changes.bizplaceId = changes.company
|
|
237
|
-
if (changes.totalUomValue && !changes.uomValue) changes.uomValue = Number(changes.totalUomValue)
|
|
238
|
-
if (changes.expDate && !changes.expirationDate) changes.expirationDate = changes.expDate
|
|
239
|
-
if (changes.location && typeof changes.location === 'string') {
|
|
240
|
-
changes.locationName = changes.location
|
|
241
|
-
delete changes.location
|
|
242
|
-
}
|
|
243
|
-
|
|
244
421
|
// Build InventoryPatch with supported fields only
|
|
245
422
|
const patch: InventoryPatch = {}
|
|
246
423
|
|
|
247
424
|
assignIfDefined(patch, 'bizplace', changes.bizplace)
|
|
248
425
|
if (changes.bizplaceId) patch.bizplace = { id: String(changes.bizplaceId) } as any
|
|
249
426
|
|
|
250
|
-
// Identify product by
|
|
251
|
-
if (changes.productDetailId) assignIfDefined(patch, 'productDetailId', String(changes.productDetailId))
|
|
427
|
+
// Identify product by sku
|
|
252
428
|
if (changes.sku) assignIfDefined(patch, 'sku', String(changes.sku))
|
|
253
429
|
if (changes.productName) assignIfDefined(patch, 'productName', String(changes.productName))
|
|
254
430
|
|
|
255
431
|
// Location can be id or name
|
|
256
|
-
if (changes.locationId) patch.location = { id: String(changes.locationId) } as any
|
|
257
432
|
if (changes.locationName) patch.location = { name: String(changes.locationName) } as any
|
|
258
433
|
|
|
259
434
|
assignIfDefined(patch, 'batchId', changes.batchId)
|
|
@@ -264,7 +439,6 @@ function massageChanges(input: any): InventoryPatch {
|
|
|
264
439
|
if (defined(changes.qty)) patch.qty = Number(changes.qty)
|
|
265
440
|
if (defined(changes.uomValue)) patch.uomValue = Number(changes.uomValue)
|
|
266
441
|
if (defined(changes.uom)) patch.uom = String(changes.uom).trim()
|
|
267
|
-
assignIfDefined(patch, 'serialNumbers', changes.serialNumbers)
|
|
268
442
|
assignIfDefined(patch, 'remark', changes.remark)
|
|
269
443
|
assignIfDefined(patch, 'expirationDate', changes.expirationDate)
|
|
270
444
|
assignIfDefined(patch, 'manufactureDate', changes.manufactureDate)
|
|
@@ -275,6 +449,25 @@ function massageChanges(input: any): InventoryPatch {
|
|
|
275
449
|
return patch
|
|
276
450
|
}
|
|
277
451
|
|
|
452
|
+
function productRequiresSerialTracking(product?: Product | null): boolean {
|
|
453
|
+
return !!(product?.isRequireSerialNumberScanning || product?.isRequireSerialNumberScanningInbound)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeSerialNumbersValue(serialNumbers: string | string[]): string {
|
|
457
|
+
if (Array.isArray(serialNumbers)) {
|
|
458
|
+
return serialNumbers
|
|
459
|
+
.map(sn => String(sn || '').trim())
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join(',')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return String(serialNumbers || '')
|
|
465
|
+
.split(',')
|
|
466
|
+
.map(sn => sn.trim())
|
|
467
|
+
.filter(Boolean)
|
|
468
|
+
.join(',')
|
|
469
|
+
}
|
|
470
|
+
|
|
278
471
|
function assignIfDefined<T extends object, K extends keyof any>(obj: T, key: K, value: any) {
|
|
279
472
|
if (defined(value)) (obj as any)[key] = value
|
|
280
473
|
}
|