@things-factory/operato-hub 4.3.701 → 4.3.708
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 +188 -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 +220 -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,155 @@ 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 (
|
|
350
|
+
iv.expiration_date is null
|
|
351
|
+
or
|
|
352
|
+
case
|
|
353
|
+
when ${shelfLifeParamIndex}::integer is not null and ${shelfLifeParamIndex}::integer > 0 then
|
|
354
|
+
CURRENT_DATE < iv.expiration_date - ${shelfLifeParamIndex}::integer
|
|
355
|
+
when p.min_outbound_shelf_life is not null and p.min_outbound_shelf_life > 0 then
|
|
356
|
+
CURRENT_DATE < iv.expiration_date - p.min_outbound_shelf_life
|
|
357
|
+
else
|
|
358
|
+
true
|
|
359
|
+
end
|
|
360
|
+
)
|
|
361
|
+
${batchFilter}
|
|
362
|
+
${warehouseFilter}
|
|
363
|
+
${lockInventoryCheck}
|
|
364
|
+
`
|
|
365
|
+
|
|
366
|
+
// Build parameters array
|
|
367
|
+
const params: any[] = [
|
|
368
|
+
domain.id,
|
|
369
|
+
customerBizplace.id,
|
|
370
|
+
item.product.id,
|
|
371
|
+
item.packingType,
|
|
372
|
+
item.packingSize,
|
|
373
|
+
item.uom
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
if (hasBatch) {
|
|
377
|
+
params.push(batchId)
|
|
378
|
+
}
|
|
379
|
+
if (requireWarehouseJoin || hasWarehouse) {
|
|
380
|
+
params.push(warehouseName)
|
|
381
|
+
}
|
|
382
|
+
params.push(releaseShelfLifeOverride)
|
|
383
|
+
|
|
384
|
+
return { query, params }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Helper function to validate inventory availability
|
|
388
|
+
const validateInventoryAvailability = async (item: any, query: string, params: any[], tx: EntityManager) => {
|
|
389
|
+
const rows = await tx.query(query, params)
|
|
390
|
+
|
|
391
|
+
const totalQty = parseFloat(rows?.[0]?.total_available_qty || '0')
|
|
392
|
+
const totalUomVal = parseFloat(rows?.[0]?.total_available_uom_value || '0')
|
|
393
|
+
|
|
394
|
+
let reqQty = item.releaseQty
|
|
395
|
+
let reqUomVal = item.releaseUomValue
|
|
396
|
+
|
|
397
|
+
if (!item.product.isInventoryDecimal) {
|
|
398
|
+
reqQty = Math.round(reqQty)
|
|
399
|
+
} else {
|
|
400
|
+
reqQty = Math.round(reqQty * 1000) / 1000
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (totalQty + 1e-6 < reqQty || totalUomVal + 1e-6 < reqUomVal) {
|
|
404
|
+
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
238
408
|
//inv without batchId will check stock in wboi
|
|
239
409
|
if (invWithoutBatchId) {
|
|
240
410
|
// validation
|
|
241
411
|
await Promise.all(
|
|
242
412
|
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
|
-
)
|
|
413
|
+
const warehouseName = item.warehouseCode ? String(item.warehouseCode).trim() : null
|
|
292
414
|
|
|
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
|
-
}
|
|
415
|
+
const { query, params } = buildInventoryAvailabilityQuery(item, domain, customerBizplace, {
|
|
416
|
+
warehouseName,
|
|
417
|
+
releaseShelfLifeOverride,
|
|
418
|
+
requireWarehouseJoin: false,
|
|
419
|
+
includeLockInventoryCheck: true
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
await validateInventoryAvailability(item, query, params, tx)
|
|
382
423
|
})
|
|
383
424
|
)
|
|
384
425
|
}
|
|
@@ -392,59 +433,15 @@ router.post(
|
|
|
392
433
|
|
|
393
434
|
const warehouseName = item?.warehouseCode ? String(item.warehouseCode).trim() : null
|
|
394
435
|
|
|
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')
|
|
436
|
+
const { query, params } = buildInventoryAvailabilityQuery(item, domain, customerBizplace, {
|
|
437
|
+
batchId,
|
|
438
|
+
warehouseName,
|
|
439
|
+
releaseShelfLifeOverride,
|
|
440
|
+
requireWarehouseJoin: true,
|
|
441
|
+
includeLockInventoryCheck: false
|
|
442
|
+
})
|
|
435
443
|
|
|
436
|
-
|
|
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
|
-
}
|
|
444
|
-
|
|
445
|
-
if (totalQty + 1e-6 < reqQty) {
|
|
446
|
-
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
447
|
-
}
|
|
444
|
+
await validateInventoryAvailability(item, query, params, tx)
|
|
448
445
|
})
|
|
449
446
|
)
|
|
450
447
|
}
|
|
@@ -456,7 +453,8 @@ router.post(
|
|
|
456
453
|
customerBizplace,
|
|
457
454
|
context,
|
|
458
455
|
tx,
|
|
459
|
-
worksheetPickingAssignment
|
|
456
|
+
worksheetPickingAssignment,
|
|
457
|
+
releaseShelfLifeOverride
|
|
460
458
|
)
|
|
461
459
|
// console.timeEnd('assign')
|
|
462
460
|
|
|
@@ -765,6 +763,7 @@ async function createReleaseGood(
|
|
|
765
763
|
: OrderNoGenerator.releaseGood(),
|
|
766
764
|
domain: domain,
|
|
767
765
|
bizplace: bizplace,
|
|
766
|
+
deliverTo: releaseGood?.deliverTo || null,
|
|
768
767
|
collectionOrderNo: releaseGood.collectionOrderNo,
|
|
769
768
|
courierOption: courierOption,
|
|
770
769
|
codOption: releaseGood?.codOption,
|
|
@@ -1247,7 +1246,8 @@ async function assignToInventory(
|
|
|
1247
1246
|
customerBizplace,
|
|
1248
1247
|
context,
|
|
1249
1248
|
tx,
|
|
1250
|
-
worksheetPickingAssignment
|
|
1249
|
+
worksheetPickingAssignment,
|
|
1250
|
+
releaseShelfLifeOverride: number | null
|
|
1251
1251
|
) {
|
|
1252
1252
|
const { domain, user } = context.state
|
|
1253
1253
|
|
|
@@ -1381,6 +1381,9 @@ async function assignToInventory(
|
|
|
1381
1381
|
if (orderInventory[oiIdx]?.warehouseCode) {
|
|
1382
1382
|
params.push(String(orderInventory[oiIdx].warehouseCode).trim())
|
|
1383
1383
|
}
|
|
1384
|
+
// add release shelf life override parameter
|
|
1385
|
+
const releaseShelfLifeParamIndex = params.length + 1
|
|
1386
|
+
params.push(releaseShelfLifeOverride)
|
|
1384
1387
|
|
|
1385
1388
|
let query = `
|
|
1386
1389
|
update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
|
|
@@ -1429,7 +1432,18 @@ async function assignToInventory(
|
|
|
1429
1432
|
"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
1433
|
AND ${batchId ? `"iv"."batch_id" = $11` : `1=1`}
|
|
1431
1434
|
${warehouseNameFilter}
|
|
1432
|
-
AND "iv"."obsolete" = false AND
|
|
1435
|
+
AND "iv"."obsolete" = false AND (
|
|
1436
|
+
"iv"."expiration_date" IS NULL
|
|
1437
|
+
OR
|
|
1438
|
+
CASE
|
|
1439
|
+
WHEN $${releaseShelfLifeParamIndex}::integer IS NOT NULL AND $${releaseShelfLifeParamIndex}::integer > 0 THEN
|
|
1440
|
+
CURRENT_DATE < ("iv"."expiration_date" - $${releaseShelfLifeParamIndex}::integer)
|
|
1441
|
+
WHEN "p"."min_outbound_shelf_life" IS NOT NULL AND "p"."min_outbound_shelf_life" > 0 THEN
|
|
1442
|
+
CURRENT_DATE < ("iv"."expiration_date" - "p"."min_outbound_shelf_life")
|
|
1443
|
+
ELSE
|
|
1444
|
+
TRUE
|
|
1445
|
+
END
|
|
1446
|
+
)
|
|
1433
1447
|
${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
|
|
1434
1448
|
ORDER BY wiar.rank ${sortQuery ? ', ' + sortQuery : ''}
|
|
1435
1449
|
)
|
|
@@ -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) {
|