@things-factory/operato-hub 4.3.701 → 4.3.705
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-server/routers/api/restful-apis/v1/company/add-contact-points.js +24 -0
- package/dist-server/routers/api/restful-apis/v1/company/add-contact-points.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/utils/params.js +10 -1
- package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js +183 -194
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-release-order.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js +18 -5
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js.map +1 -1
- package/openapi/v1/contact-point.yaml +37 -48
- package/package.json +58 -58
- package/server/routers/api/restful-apis/v1/company/add-contact-points.ts +29 -1
- package/server/routers/api/restful-apis/v1/utils/params.ts +10 -1
- package/server/routers/api/restful-apis/v1/warehouse/add-release-order.ts +215 -206
- package/server/routers/api/restful-apis/v1/warehouse/update-release-order-details.ts +19 -5
|
@@ -26,11 +26,15 @@ router.post(
|
|
|
26
26
|
// check if phone or email already belongs in bizplace
|
|
27
27
|
const phones = []
|
|
28
28
|
const emails = []
|
|
29
|
+
const ids = []
|
|
29
30
|
let values = []
|
|
30
31
|
for (const datapoint of context.request.body.data) {
|
|
31
32
|
phones.push(datapoint.phone)
|
|
32
33
|
emails.push(datapoint.email)
|
|
33
|
-
|
|
34
|
+
if (datapoint.id) {
|
|
35
|
+
ids.push(datapoint.id)
|
|
36
|
+
}
|
|
37
|
+
const value: any = {
|
|
34
38
|
name: datapoint.name,
|
|
35
39
|
description: datapoint.description,
|
|
36
40
|
companyName: datapoint.companyName,
|
|
@@ -43,6 +47,8 @@ router.post(
|
|
|
43
47
|
address2: datapoint.address2,
|
|
44
48
|
city: datapoint.city,
|
|
45
49
|
state: datapoint.state,
|
|
50
|
+
country: datapoint.country,
|
|
51
|
+
releaseShelfLife: datapoint.releaseShelfLife,
|
|
46
52
|
postCode: datapoint.postCode,
|
|
47
53
|
type: datapoint.type,
|
|
48
54
|
domain: context.state.domain,
|
|
@@ -50,9 +56,28 @@ router.post(
|
|
|
50
56
|
creator: context.state.user,
|
|
51
57
|
updater: context.state.user
|
|
52
58
|
}
|
|
59
|
+
// Add id if provided by external system
|
|
60
|
+
if (datapoint.id) {
|
|
61
|
+
value.id = datapoint.id
|
|
62
|
+
}
|
|
53
63
|
values.push(value)
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
// Check for duplicate IDs if provided
|
|
67
|
+
if (ids.length > 0) {
|
|
68
|
+
const checkDuplicateIds: ContactPoint[] = await tx
|
|
69
|
+
.getRepository(ContactPoint)
|
|
70
|
+
.createQueryBuilder('cp')
|
|
71
|
+
.where('cp.id IN (:...ids)', { ids })
|
|
72
|
+
.getMany()
|
|
73
|
+
if (checkDuplicateIds.length > 0) {
|
|
74
|
+
throw new ApiError(
|
|
75
|
+
'E05',
|
|
76
|
+
`Contact point ID(s) already exist: ${checkDuplicateIds.map(cp => cp.id).join(', ')}`
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
const checkDuplicateEmailPhone: ContactPoint = await tx
|
|
57
82
|
.getRepository(ContactPoint)
|
|
58
83
|
.createQueryBuilder('cp')
|
|
@@ -80,6 +105,7 @@ router.post(
|
|
|
80
105
|
let data = []
|
|
81
106
|
for (const contactPoint of result.generatedMaps) {
|
|
82
107
|
const datapoint = {
|
|
108
|
+
id: contactPoint.id,
|
|
83
109
|
name: contactPoint.name,
|
|
84
110
|
description: contactPoint.description,
|
|
85
111
|
companyName: contactPoint.companyName,
|
|
@@ -92,6 +118,8 @@ router.post(
|
|
|
92
118
|
address2: contactPoint.address2,
|
|
93
119
|
city: contactPoint.city,
|
|
94
120
|
state: contactPoint.state,
|
|
121
|
+
country: contactPoint.country,
|
|
122
|
+
releaseShelfLife: contactPoint.releaseShelfLife,
|
|
95
123
|
postCode: contactPoint.postCode,
|
|
96
124
|
type: contactPoint.type,
|
|
97
125
|
createdAt: contactPoint.createdAt
|
|
@@ -83,6 +83,7 @@ export const params = {
|
|
|
83
83
|
'get-webhook-details': ['warehouseBizplaceId', 'webhookId'],
|
|
84
84
|
'add-contact-points': [
|
|
85
85
|
'bizplaceId',
|
|
86
|
+
'id',
|
|
86
87
|
'name',
|
|
87
88
|
'companyName',
|
|
88
89
|
'description',
|
|
@@ -95,6 +96,8 @@ export const params = {
|
|
|
95
96
|
'address2',
|
|
96
97
|
'city',
|
|
97
98
|
'state',
|
|
99
|
+
'country',
|
|
100
|
+
'releaseShelfLife',
|
|
98
101
|
'postCode',
|
|
99
102
|
'type'
|
|
100
103
|
],
|
|
@@ -358,6 +361,7 @@ export const params = {
|
|
|
358
361
|
'refNo2',
|
|
359
362
|
'refNo3',
|
|
360
363
|
'refOrderId',
|
|
364
|
+
'contactPointId',
|
|
361
365
|
'type',
|
|
362
366
|
'transporter',
|
|
363
367
|
'trackingNo',
|
|
@@ -1112,6 +1116,11 @@ export const reqParams = {
|
|
|
1112
1116
|
required: true,
|
|
1113
1117
|
type: 'field'
|
|
1114
1118
|
},
|
|
1119
|
+
{
|
|
1120
|
+
name: 'contactPointId',
|
|
1121
|
+
required: false,
|
|
1122
|
+
type: 'field'
|
|
1123
|
+
},
|
|
1115
1124
|
{
|
|
1116
1125
|
name: 'shippingOrder',
|
|
1117
1126
|
required: false,
|
|
@@ -1201,7 +1210,7 @@ export const reqParams = {
|
|
|
1201
1210
|
name: 'releaseGood',
|
|
1202
1211
|
required: true,
|
|
1203
1212
|
type: 'object',
|
|
1204
|
-
data: [
|
|
1213
|
+
data: []
|
|
1205
1214
|
},
|
|
1206
1215
|
{
|
|
1207
1216
|
name: 'shippingOrder',
|
|
@@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|
|
13
13
|
import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
|
|
14
14
|
import { restfulApiRouter as router } from '@things-factory/api'
|
|
15
15
|
import { User } from '@things-factory/auth-base'
|
|
16
|
-
import { Bizplace, getCompanyBizplace } from '@things-factory/biz-base'
|
|
16
|
+
import { Bizplace, ContactPoint, getCompanyBizplace } from '@things-factory/biz-base'
|
|
17
17
|
import { GeoCountry } from '@things-factory/geography'
|
|
18
18
|
import { generateId } from '@things-factory/id-rule-base'
|
|
19
19
|
import { LastMileDelivery } from '@things-factory/integration-lmd'
|
|
@@ -87,6 +87,23 @@ router.post(
|
|
|
87
87
|
relations: ['domain']
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
+
// optional override: release shelf life from ContactPoint
|
|
91
|
+
const contactPointId: string | undefined = bodyReq?.contactPointId
|
|
92
|
+
let releaseShelfLifeOverride: number | null = null
|
|
93
|
+
let deliverToContactPoint: ContactPoint | null = null
|
|
94
|
+
if (contactPointId) {
|
|
95
|
+
const contactPoint: ContactPoint | undefined = await tx
|
|
96
|
+
.getRepository(ContactPoint)
|
|
97
|
+
.findOne({ where: { id: contactPointId } })
|
|
98
|
+
if (!contactPoint) {
|
|
99
|
+
throw new ApiError('E04', 'contactPoint not found')
|
|
100
|
+
}
|
|
101
|
+
if (contactPoint?.releaseShelfLife != null && contactPoint.releaseShelfLife !== 0) {
|
|
102
|
+
releaseShelfLifeOverride = contactPoint.releaseShelfLife
|
|
103
|
+
}
|
|
104
|
+
deliverToContactPoint = contactPoint
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
const worksheetPickingAssignment: Setting = await tx.getRepository(Setting).findOne({
|
|
91
108
|
where: { domain, category: 'id-rule', name: 'enable-worksheet-picking-activation-assignment' }
|
|
92
109
|
})
|
|
@@ -105,7 +122,12 @@ router.post(
|
|
|
105
122
|
})
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
|
|
125
|
+
// derive phone values: prefer contact point when provided, else use body
|
|
126
|
+
let newPhone1 = deliverToContactPoint?.phone
|
|
127
|
+
? String(deliverToContactPoint.phone).replace(/\D/g, '')
|
|
128
|
+
: bodyReq?.deliverTo?.phone1
|
|
129
|
+
? bodyReq.deliverTo.phone1.replace(/\D/g, '')
|
|
130
|
+
: null
|
|
109
131
|
let newPhone2 = bodyReq?.deliverTo?.phone2 ? bodyReq.deliverTo.phone2.replace(/\D/g, '') : null
|
|
110
132
|
|
|
111
133
|
releaseGood = {
|
|
@@ -126,23 +148,36 @@ router.post(
|
|
|
126
148
|
type: bodyReq.type,
|
|
127
149
|
marketplaceOrderStatus: bodyReq?.marketplaceOrderStatus,
|
|
128
150
|
remark: bodyReq?.remark || null,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
// when contactPoint exists, override target fields from contactPoint, else use request body
|
|
152
|
+
billingAddress: deliverToContactPoint
|
|
153
|
+
? deliverToContactPoint.billingAddress || null
|
|
154
|
+
: bodyReq?.billTo?.billingAddress || null,
|
|
155
|
+
deliveryAddress1: deliverToContactPoint
|
|
156
|
+
? deliverToContactPoint.address || null
|
|
157
|
+
: bodyReq?.deliverTo?.deliveryAddress1 || null,
|
|
158
|
+
deliveryAddress2: deliverToContactPoint
|
|
159
|
+
? deliverToContactPoint.address2 || null
|
|
160
|
+
: bodyReq?.deliverTo?.deliveryAddress2 || null,
|
|
132
161
|
deliveryAddress3: bodyReq?.deliverTo?.deliveryAddress3 || null,
|
|
133
162
|
deliveryAddress4: bodyReq?.deliverTo?.deliveryAddress4 || null,
|
|
134
163
|
deliveryAddress5: bodyReq?.deliverTo?.deliveryAddress5 || null,
|
|
135
|
-
attentionTo:
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
attentionTo: deliverToContactPoint
|
|
165
|
+
? deliverToContactPoint.name || null
|
|
166
|
+
: bodyReq?.deliverTo?.attentionTo || null,
|
|
167
|
+
attentionCompany: deliverToContactPoint
|
|
168
|
+
? deliverToContactPoint.companyName || null
|
|
169
|
+
: bodyReq?.deliverTo?.attentionCompany || null,
|
|
170
|
+
city: deliverToContactPoint ? deliverToContactPoint.city || null : bodyReq?.deliverTo?.city || null,
|
|
138
171
|
ward: bodyReq?.deliverTo?.ward,
|
|
139
|
-
district: bodyReq?.deliverTo?.district,
|
|
140
|
-
state: bodyReq?.deliverTo?.state || null,
|
|
141
|
-
postalCode:
|
|
142
|
-
|
|
172
|
+
district: deliverToContactPoint ? null : bodyReq?.deliverTo?.district,
|
|
173
|
+
state: deliverToContactPoint ? deliverToContactPoint.state || null : bodyReq?.deliverTo?.state || null,
|
|
174
|
+
postalCode: deliverToContactPoint
|
|
175
|
+
? deliverToContactPoint.postCode || null
|
|
176
|
+
: bodyReq?.deliverTo?.postalCode || null,
|
|
177
|
+
country: deliverToContactPoint ? null : bodyReq?.deliverTo?.country || null,
|
|
143
178
|
phone1: newPhone1,
|
|
144
179
|
phone2: newPhone2,
|
|
145
|
-
email: bodyReq?.deliverTo?.email || null,
|
|
180
|
+
email: deliverToContactPoint ? deliverToContactPoint.email || null : bodyReq?.deliverTo?.email || null,
|
|
146
181
|
transporter: bodyReq?.transporter,
|
|
147
182
|
trackingNo: bodyReq?.trackingNo,
|
|
148
183
|
airwayBill: bodyReq?.airwayBill,
|
|
@@ -162,6 +197,7 @@ router.post(
|
|
|
162
197
|
shippingFee: bodyReq?.shippingFee,
|
|
163
198
|
lmdOption: bodyReq?.lmdOption,
|
|
164
199
|
priorityDelivery: bodyReq?.priorityDelivery,
|
|
200
|
+
deliverTo: deliverToContactPoint || null,
|
|
165
201
|
shippingOrder: bodyReq?.shippingOrder
|
|
166
202
|
? {
|
|
167
203
|
shipName: bodyReq?.shippingOrder.shipName,
|
|
@@ -235,150 +271,156 @@ router.post(
|
|
|
235
271
|
|
|
236
272
|
const invWithoutBatchId = batchIdStates.withoutBatchId
|
|
237
273
|
|
|
274
|
+
// Helper function to build inventory availability query
|
|
275
|
+
const buildInventoryAvailabilityQuery = (
|
|
276
|
+
item: any,
|
|
277
|
+
domain: Domain,
|
|
278
|
+
customerBizplace: Bizplace,
|
|
279
|
+
options: {
|
|
280
|
+
batchId?: string | null
|
|
281
|
+
warehouseName?: string | null
|
|
282
|
+
releaseShelfLifeOverride: number | null
|
|
283
|
+
requireWarehouseJoin?: boolean
|
|
284
|
+
includeLockInventoryCheck?: boolean
|
|
285
|
+
}
|
|
286
|
+
) => {
|
|
287
|
+
const { batchId, warehouseName, releaseShelfLifeOverride, requireWarehouseJoin, includeLockInventoryCheck } =
|
|
288
|
+
options
|
|
289
|
+
|
|
290
|
+
const hasWarehouse = warehouseName != null
|
|
291
|
+
const hasBatch = batchId != null
|
|
292
|
+
|
|
293
|
+
// Determine warehouse join strategy
|
|
294
|
+
const warehouseJoin =
|
|
295
|
+
requireWarehouseJoin || hasWarehouse ? 'inner join warehouses w on w.id = loc.warehouse_id' : ''
|
|
296
|
+
|
|
297
|
+
// Build warehouse filter
|
|
298
|
+
let warehouseFilter = ''
|
|
299
|
+
if (requireWarehouseJoin) {
|
|
300
|
+
// For batch queries: always join warehouses, filter optionally
|
|
301
|
+
warehouseFilter = 'and ($8::text is null or w.name = $8::text)'
|
|
302
|
+
} else if (hasWarehouse) {
|
|
303
|
+
// For non-batch queries: only filter if warehouse is specified
|
|
304
|
+
warehouseFilter = 'and w.name = $7'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Build batch filter
|
|
308
|
+
const batchFilter = hasBatch ? 'and iv.batch_id = $7' : ''
|
|
309
|
+
|
|
310
|
+
// Calculate parameter indices
|
|
311
|
+
const shelfLifeParamIndex = hasBatch ? '$9' : hasWarehouse ? '$8' : '$7'
|
|
312
|
+
|
|
313
|
+
// Build lock inventory check
|
|
314
|
+
const lockInventoryCheck = includeLockInventoryCheck
|
|
315
|
+
? `and not exists (
|
|
316
|
+
select 1 from inventories i
|
|
317
|
+
where i.domain_id = $1 and i.bizplace_id = $2 and i.product_id = $3 and i.packing_type = $4 and i.packing_size = $5 and i.uom = $6 and i.status = 'STORED' and i.lock_inventory is true
|
|
318
|
+
)`
|
|
319
|
+
: ''
|
|
320
|
+
|
|
321
|
+
const query = `
|
|
322
|
+
select
|
|
323
|
+
coalesce(sum(
|
|
324
|
+
case when (iv.qty - greatest(coalesce(iv.locked_qty,0),0) - greatest(coalesce(pds.unassigned_qty,0),0)) < 0
|
|
325
|
+
then 0
|
|
326
|
+
else (iv.qty - greatest(coalesce(iv.locked_qty,0),0) - greatest(coalesce(pds.unassigned_qty,0),0))
|
|
327
|
+
end
|
|
328
|
+
), 0) as total_available_qty,
|
|
329
|
+
coalesce(sum(
|
|
330
|
+
case when (iv.uom_value - greatest(coalesce(iv.locked_uom_value,0),0) - greatest(coalesce(pds.unassigned_uom_value,0),0)) < 0
|
|
331
|
+
then 0
|
|
332
|
+
else (iv.uom_value - greatest(coalesce(iv.locked_uom_value,0),0) - greatest(coalesce(pds.unassigned_uom_value,0),0))
|
|
333
|
+
end
|
|
334
|
+
), 0) as total_available_uom_value
|
|
335
|
+
from inventories iv
|
|
336
|
+
left join product_detail_stocks pds on pds.product_detail_id = iv.product_detail_id
|
|
337
|
+
inner join locations loc on loc.id = iv.location_id
|
|
338
|
+
${warehouseJoin}
|
|
339
|
+
inner join products p on p.id = iv.product_id
|
|
340
|
+
where iv.domain_id = $1
|
|
341
|
+
and iv.bizplace_id = $2
|
|
342
|
+
and iv.product_id = $3
|
|
343
|
+
and iv.packing_type = $4
|
|
344
|
+
and iv.packing_size = $5
|
|
345
|
+
and iv.uom = $6
|
|
346
|
+
and iv.status = 'STORED'
|
|
347
|
+
and loc.type not in ('QUARANTINE','RESERVE','DAMAGE','STORAGE')
|
|
348
|
+
and iv.obsolete = false
|
|
349
|
+
and (case
|
|
350
|
+
when iv.expiration_date is not null
|
|
351
|
+
then CURRENT_DATE < iv.expiration_date - (
|
|
352
|
+
case
|
|
353
|
+
when ${shelfLifeParamIndex}::integer is not null and ${shelfLifeParamIndex}::integer > 0
|
|
354
|
+
then ${shelfLifeParamIndex}::integer
|
|
355
|
+
when p.min_outbound_shelf_life is not null
|
|
356
|
+
then p.min_outbound_shelf_life
|
|
357
|
+
else 0
|
|
358
|
+
end
|
|
359
|
+
)
|
|
360
|
+
else true
|
|
361
|
+
end)
|
|
362
|
+
${batchFilter}
|
|
363
|
+
${warehouseFilter}
|
|
364
|
+
${lockInventoryCheck}
|
|
365
|
+
`
|
|
366
|
+
|
|
367
|
+
// Build parameters array
|
|
368
|
+
const params: any[] = [
|
|
369
|
+
domain.id,
|
|
370
|
+
customerBizplace.id,
|
|
371
|
+
item.product.id,
|
|
372
|
+
item.packingType,
|
|
373
|
+
item.packingSize,
|
|
374
|
+
item.uom
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
if (hasBatch) {
|
|
378
|
+
params.push(batchId)
|
|
379
|
+
}
|
|
380
|
+
if (requireWarehouseJoin || hasWarehouse) {
|
|
381
|
+
params.push(warehouseName)
|
|
382
|
+
}
|
|
383
|
+
params.push(releaseShelfLifeOverride)
|
|
384
|
+
|
|
385
|
+
return { query, params }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Helper function to validate inventory availability
|
|
389
|
+
const validateInventoryAvailability = async (item: any, query: string, params: any[], tx: EntityManager) => {
|
|
390
|
+
const rows = await tx.query(query, params)
|
|
391
|
+
|
|
392
|
+
const totalQty = parseFloat(rows?.[0]?.total_available_qty || '0')
|
|
393
|
+
const totalUomVal = parseFloat(rows?.[0]?.total_available_uom_value || '0')
|
|
394
|
+
|
|
395
|
+
let reqQty = item.releaseQty
|
|
396
|
+
let reqUomVal = item.releaseUomValue
|
|
397
|
+
|
|
398
|
+
if (!item.product.isInventoryDecimal) {
|
|
399
|
+
reqQty = Math.round(reqQty)
|
|
400
|
+
} else {
|
|
401
|
+
reqQty = Math.round(reqQty * 1000) / 1000
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (totalQty + 1e-6 < reqQty || totalUomVal + 1e-6 < reqUomVal) {
|
|
405
|
+
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
238
409
|
//inv without batchId will check stock in wboi
|
|
239
410
|
if (invWithoutBatchId) {
|
|
240
411
|
// validation
|
|
241
412
|
await Promise.all(
|
|
242
413
|
massagedData.combinedItems.map(async item => {
|
|
243
|
-
const
|
|
244
|
-
if (hasWarehouse) {
|
|
245
|
-
// Validate availability within the specific warehouse across pickable locations (exclude Q/R/D/STORAGE)
|
|
246
|
-
const rows = await tx.query(
|
|
247
|
-
`
|
|
248
|
-
select
|
|
249
|
-
coalesce(sum(
|
|
250
|
-
case when (iv.qty - greatest(coalesce(iv.locked_qty,0),0) - greatest(coalesce(pds.unassigned_qty,0),0)) < 0
|
|
251
|
-
then 0
|
|
252
|
-
else (iv.qty - greatest(coalesce(iv.locked_qty,0),0) - greatest(coalesce(pds.unassigned_qty,0),0))
|
|
253
|
-
end
|
|
254
|
-
), 0) as total_available_qty,
|
|
255
|
-
coalesce(sum(
|
|
256
|
-
case when (iv.uom_value - greatest(coalesce(iv.locked_uom_value,0),0) - greatest(coalesce(pds.unassigned_uom_value,0),0)) < 0
|
|
257
|
-
then 0
|
|
258
|
-
else (iv.uom_value - greatest(coalesce(iv.locked_uom_value,0),0) - greatest(coalesce(pds.unassigned_uom_value,0),0))
|
|
259
|
-
end
|
|
260
|
-
), 0) as total_available_uom_value
|
|
261
|
-
from inventories iv
|
|
262
|
-
left join product_detail_stocks pds on pds.product_detail_id = iv.product_detail_id
|
|
263
|
-
inner join locations loc on loc.id = iv.location_id
|
|
264
|
-
inner join warehouses w on w.id = loc.warehouse_id
|
|
265
|
-
inner join products p on p.id = iv.product_id
|
|
266
|
-
where iv.domain_id = $1
|
|
267
|
-
and iv.bizplace_id = $2
|
|
268
|
-
and iv.product_id = $3
|
|
269
|
-
and iv.packing_type = $4
|
|
270
|
-
and iv.packing_size = $5
|
|
271
|
-
and iv.uom = $6
|
|
272
|
-
and iv.status = 'STORED'
|
|
273
|
-
and loc.type not in ('QUARANTINE','RESERVE','DAMAGE','STORAGE')
|
|
274
|
-
and iv.obsolete = false
|
|
275
|
-
and (case when iv.expiration_date is not null and p.min_outbound_shelf_life is not null then CURRENT_DATE < iv.expiration_date - p.min_outbound_shelf_life else true end)
|
|
276
|
-
and w.name = $7
|
|
277
|
-
and not exists (
|
|
278
|
-
select 1 from inventories i
|
|
279
|
-
where i.domain_id = $1 and i.bizplace_id = $2 and i.product_id = $3 and i.packing_type = $4 and i.packing_size = $5 and i.uom = $6 and i.status = 'STORED' and i.lock_inventory is true
|
|
280
|
-
)
|
|
281
|
-
`,
|
|
282
|
-
[
|
|
283
|
-
domain.id,
|
|
284
|
-
customerBizplace.id,
|
|
285
|
-
item.product.id,
|
|
286
|
-
item.packingType,
|
|
287
|
-
item.packingSize,
|
|
288
|
-
item.uom,
|
|
289
|
-
String(item.warehouseCode).trim()
|
|
290
|
-
]
|
|
291
|
-
)
|
|
414
|
+
const warehouseName = item.warehouseCode ? String(item.warehouseCode).trim() : null
|
|
292
415
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
} else {
|
|
302
|
-
reqQty = Math.round(reqQty * 1000) / 1000
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (totalQty + 1e-6 < reqQty || totalUomVal + 1e-6 < reqUomVal) {
|
|
306
|
-
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
// Fallback to existing global view-based validation when no warehouseCode provided
|
|
310
|
-
let itemList = await tx.query(
|
|
311
|
-
`
|
|
312
|
-
select wboi.*
|
|
313
|
-
from warehouse_bizplace_onhand_inventories wboi
|
|
314
|
-
left join (
|
|
315
|
-
select i2.product_id, i2.domain_id, i2.bizplace_id, i2.packing_type, i2.packing_size, i2.uom,
|
|
316
|
-
sum(i2.qty) as storage_qty,
|
|
317
|
-
sum(i2.uom_value) as storage_uom_value
|
|
318
|
-
from inventories i2
|
|
319
|
-
inner join locations l2 on l2.id = i2.location_id
|
|
320
|
-
inner join warehouses w on w.id = l2.warehouse_id
|
|
321
|
-
where i2.domain_id = $1
|
|
322
|
-
and i2.bizplace_id = $2
|
|
323
|
-
and i2.product_id = $3
|
|
324
|
-
and i2.packing_type = $4
|
|
325
|
-
and i2.packing_size = $5
|
|
326
|
-
and i2.uom = $6
|
|
327
|
-
and i2.status = 'STORED'
|
|
328
|
-
and l2.type = 'STORAGE'
|
|
329
|
-
group by i2.product_id, i2.domain_id, i2.bizplace_id, i2.packing_type, i2.packing_size, i2.uom
|
|
330
|
-
) storageInv
|
|
331
|
-
on storageInv.product_id = wboi.product_id
|
|
332
|
-
and storageInv.domain_id = wboi.domain_id
|
|
333
|
-
and storageInv.bizplace_id = wboi.bizplace_id
|
|
334
|
-
and storageInv.packing_type = wboi.packing_type
|
|
335
|
-
and storageInv.packing_size = wboi.packing_size
|
|
336
|
-
and storageInv.uom = wboi.uom
|
|
337
|
-
left join (
|
|
338
|
-
select i.product_id, i.domain_id, i.bizplace_id, i.packing_type, i.packing_size, i.uom, i.lock_inventory
|
|
339
|
-
from inventories i
|
|
340
|
-
where i.domain_id = $1
|
|
341
|
-
and i.bizplace_id = $2
|
|
342
|
-
and i.product_id = $3
|
|
343
|
-
and i.packing_type = $4
|
|
344
|
-
and i.packing_size = $5
|
|
345
|
-
and i.uom = $6
|
|
346
|
-
and i.status = 'STORED'
|
|
347
|
-
group by i.product_id, i.domain_id, i.bizplace_id, i.packing_type, i.packing_size, i.uom, i.lock_inventory
|
|
348
|
-
) lockInv
|
|
349
|
-
on lockInv.product_id = wboi.product_id
|
|
350
|
-
and lockInv.domain_id = wboi.domain_id
|
|
351
|
-
and lockInv.bizplace_id = wboi.bizplace_id
|
|
352
|
-
and lockInv.packing_type = wboi.packing_type
|
|
353
|
-
and lockInv.packing_size = wboi.packing_size
|
|
354
|
-
and lockInv.uom = wboi.uom
|
|
355
|
-
where wboi.domain_id = $1
|
|
356
|
-
and wboi.bizplace_id = $2
|
|
357
|
-
and wboi.group_type = 'SINGLE'
|
|
358
|
-
and wboi.product_id = $3
|
|
359
|
-
and wboi.packing_type = $4
|
|
360
|
-
and wboi.packing_size = $5
|
|
361
|
-
and wboi.uom = $6
|
|
362
|
-
and lockInv.lock_inventory is not true
|
|
363
|
-
and (wboi.remain_qty - wboi.transfer_qty - coalesce(storageInv.storage_qty, 0)) >= $7
|
|
364
|
-
and (wboi.remain_uom_value - wboi.transfer_uom_value - coalesce(storageInv.storage_uom_value, 0)) >= $8
|
|
365
|
-
`,
|
|
366
|
-
[
|
|
367
|
-
domain.id,
|
|
368
|
-
customerBizplace.id,
|
|
369
|
-
item.product.id,
|
|
370
|
-
item.packingType,
|
|
371
|
-
item.packingSize,
|
|
372
|
-
item.uom,
|
|
373
|
-
item.releaseQty,
|
|
374
|
-
item.releaseUomValue
|
|
375
|
-
]
|
|
376
|
-
)
|
|
377
|
-
if (itemList.length <= 0) {
|
|
378
|
-
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
379
|
-
}
|
|
380
|
-
console.log()
|
|
381
|
-
}
|
|
416
|
+
const { query, params } = buildInventoryAvailabilityQuery(item, domain, customerBizplace, {
|
|
417
|
+
warehouseName,
|
|
418
|
+
releaseShelfLifeOverride,
|
|
419
|
+
requireWarehouseJoin: false,
|
|
420
|
+
includeLockInventoryCheck: true
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await validateInventoryAvailability(item, query, params, tx)
|
|
382
424
|
})
|
|
383
425
|
)
|
|
384
426
|
}
|
|
@@ -392,59 +434,15 @@ router.post(
|
|
|
392
434
|
|
|
393
435
|
const warehouseName = item?.warehouseCode ? String(item.warehouseCode).trim() : null
|
|
394
436
|
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
end
|
|
403
|
-
), 0) as total_available_qty
|
|
404
|
-
from inventories iv
|
|
405
|
-
left join product_detail_stocks pds on pds.product_detail_id = iv.product_detail_id
|
|
406
|
-
inner join locations loc on loc.id = iv.location_id
|
|
407
|
-
inner join warehouses w on w.id = loc.warehouse_id
|
|
408
|
-
inner join products p on p.id = iv.product_id
|
|
409
|
-
where iv.domain_id = $1
|
|
410
|
-
and iv.bizplace_id = $2
|
|
411
|
-
and iv.product_id = $3
|
|
412
|
-
and iv.packing_type = $4
|
|
413
|
-
and iv.packing_size = $5
|
|
414
|
-
and iv.uom = $6
|
|
415
|
-
and iv.status = 'STORED'
|
|
416
|
-
and loc.type not in ('QUARANTINE','RESERVE','DAMAGE','STORAGE')
|
|
417
|
-
and iv.obsolete = false
|
|
418
|
-
and (case when iv.expiration_date is not null and p.min_outbound_shelf_life is not null then CURRENT_DATE < iv.expiration_date - p.min_outbound_shelf_life else true end)
|
|
419
|
-
and iv.batch_id = $7
|
|
420
|
-
and ( $8::text is null or w.name = $8::text )
|
|
421
|
-
`,
|
|
422
|
-
[
|
|
423
|
-
domain.id,
|
|
424
|
-
customerBizplace.id,
|
|
425
|
-
item.product.id,
|
|
426
|
-
item.packingType,
|
|
427
|
-
item.packingSize,
|
|
428
|
-
item.uom,
|
|
429
|
-
batchId,
|
|
430
|
-
warehouseName
|
|
431
|
-
]
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
const totalQty = parseFloat(rows?.[0]?.total_available_qty || '0')
|
|
435
|
-
|
|
436
|
-
let reqQty = item.releaseQty
|
|
437
|
-
|
|
438
|
-
// normalize comparison for decimal vs non-decimal products
|
|
439
|
-
if (!item.product.isInventoryDecimal) {
|
|
440
|
-
reqQty = Math.round(reqQty)
|
|
441
|
-
} else {
|
|
442
|
-
reqQty = Math.round(reqQty * 1000) / 1000
|
|
443
|
-
}
|
|
437
|
+
const { query, params } = buildInventoryAvailabilityQuery(item, domain, customerBizplace, {
|
|
438
|
+
batchId,
|
|
439
|
+
warehouseName,
|
|
440
|
+
releaseShelfLifeOverride,
|
|
441
|
+
requireWarehouseJoin: true,
|
|
442
|
+
includeLockInventoryCheck: false
|
|
443
|
+
})
|
|
444
444
|
|
|
445
|
-
|
|
446
|
-
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
447
|
-
}
|
|
445
|
+
await validateInventoryAvailability(item, query, params, tx)
|
|
448
446
|
})
|
|
449
447
|
)
|
|
450
448
|
}
|
|
@@ -456,7 +454,8 @@ router.post(
|
|
|
456
454
|
customerBizplace,
|
|
457
455
|
context,
|
|
458
456
|
tx,
|
|
459
|
-
worksheetPickingAssignment
|
|
457
|
+
worksheetPickingAssignment,
|
|
458
|
+
releaseShelfLifeOverride
|
|
460
459
|
)
|
|
461
460
|
// console.timeEnd('assign')
|
|
462
461
|
|
|
@@ -765,6 +764,7 @@ async function createReleaseGood(
|
|
|
765
764
|
: OrderNoGenerator.releaseGood(),
|
|
766
765
|
domain: domain,
|
|
767
766
|
bizplace: bizplace,
|
|
767
|
+
deliverTo: releaseGood?.deliverTo || null,
|
|
768
768
|
collectionOrderNo: releaseGood.collectionOrderNo,
|
|
769
769
|
courierOption: courierOption,
|
|
770
770
|
codOption: releaseGood?.codOption,
|
|
@@ -1247,7 +1247,8 @@ async function assignToInventory(
|
|
|
1247
1247
|
customerBizplace,
|
|
1248
1248
|
context,
|
|
1249
1249
|
tx,
|
|
1250
|
-
worksheetPickingAssignment
|
|
1250
|
+
worksheetPickingAssignment,
|
|
1251
|
+
releaseShelfLifeOverride: number | null
|
|
1251
1252
|
) {
|
|
1252
1253
|
const { domain, user } = context.state
|
|
1253
1254
|
|
|
@@ -1381,6 +1382,9 @@ async function assignToInventory(
|
|
|
1381
1382
|
if (orderInventory[oiIdx]?.warehouseCode) {
|
|
1382
1383
|
params.push(String(orderInventory[oiIdx].warehouseCode).trim())
|
|
1383
1384
|
}
|
|
1385
|
+
// add release shelf life override parameter
|
|
1386
|
+
const releaseShelfLifeParamIndex = params.length + 1
|
|
1387
|
+
params.push(releaseShelfLifeOverride)
|
|
1384
1388
|
|
|
1385
1389
|
let query = `
|
|
1386
1390
|
update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
|
|
@@ -1429,7 +1433,12 @@ async function assignToInventory(
|
|
|
1429
1433
|
"iv"."domain_id" = $2 AND "iv"."bizplace_id" = $3 AND "iv"."packing_type" = $4 AND "iv"."packing_size" = $5 AND "iv"."product_id" = $6 AND "iv"."status" = $7 AND "loc"."type" NOT IN ($8, $9, $10)
|
|
1430
1434
|
AND ${batchId ? `"iv"."batch_id" = $11` : `1=1`}
|
|
1431
1435
|
${warehouseNameFilter}
|
|
1432
|
-
AND "iv"."obsolete" = false AND case
|
|
1436
|
+
AND "iv"."obsolete" = false AND case
|
|
1437
|
+
when "iv"."expiration_date" is not null
|
|
1438
|
+
and coalesce($${releaseShelfLifeParamIndex}::integer, "p"."min_outbound_shelf_life") is not null
|
|
1439
|
+
then CURRENT_DATE < "iv"."expiration_date" - coalesce($${releaseShelfLifeParamIndex}::integer, "p"."min_outbound_shelf_life")
|
|
1440
|
+
else true
|
|
1441
|
+
end
|
|
1433
1442
|
${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
|
|
1434
1443
|
ORDER BY wiar.rank ${sortQuery ? ', ' + sortQuery : ''}
|
|
1435
1444
|
)
|
|
@@ -43,16 +43,30 @@ router.post(
|
|
|
43
43
|
async function updateReleaseGoodDetails(tx, domain, patch, user) {
|
|
44
44
|
let { releaseGood, shippingOrder } = patch
|
|
45
45
|
let status = [ORDER_STATUS.DONE, ORDER_STATUS.CANCELLED, ORDER_STATUS.REJECTED]
|
|
46
|
+
|
|
47
|
+
// Build where clause: prioritize refOrderId over roId
|
|
48
|
+
let whereClause: any = { domain }
|
|
49
|
+
const refOrderId = releaseGood?.refOrderId
|
|
50
|
+
const hasRefOrderId = refOrderId != null && refOrderId !== ''
|
|
51
|
+
|
|
52
|
+
if (hasRefOrderId) {
|
|
53
|
+
whereClause.refOrderId = refOrderId
|
|
54
|
+
} else if (releaseGood?.roId) {
|
|
55
|
+
whereClause.id = releaseGood.roId
|
|
56
|
+
} else {
|
|
57
|
+
throw new ApiError('E04', 'release order: roId or refOrderId is required')
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
let foundReleaseGood = await tx.getRepository(ReleaseGood).findOne({
|
|
47
|
-
where:
|
|
48
|
-
id: releaseGood.roId,
|
|
49
|
-
domain
|
|
50
|
-
},
|
|
61
|
+
where: whereClause,
|
|
51
62
|
relations: ['shippingOrder']
|
|
52
63
|
})
|
|
53
64
|
|
|
54
65
|
if (!foundReleaseGood) {
|
|
55
|
-
|
|
66
|
+
const searchId = hasRefOrderId
|
|
67
|
+
? `refOrderId: ${refOrderId}`
|
|
68
|
+
: `roId: ${releaseGood?.roId}`
|
|
69
|
+
throw new ApiError('E04', `release order: ${searchId}`)
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
if (status.indexOf(foundReleaseGood.status) !== -1) {
|