@things-factory/operato-wms 4.3.757 → 4.3.759
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/client/pages/outbound/loading-v2/delivery-order-info.js +38 -3
- package/client/pages/outbound/loading-v2/loading-execution-base.js +132 -17
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +15 -0
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js +9 -8
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/load-by-qty.js +115 -45
- package/dist-server/graphql/resolvers/outbound/load-by-qty.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +4 -0
- package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/undo-load.js +94 -42
- package/dist-server/graphql/resolvers/outbound/undo-load.js.map +1 -1
- package/dist-server/graphql/resolvers/route-label/route-label.js +8 -12
- package/dist-server/graphql/resolvers/route-label/route-label.js.map +1 -1
- package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js +31 -2
- package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js.map +1 -1
- package/package.json +4 -4
- package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +15 -0
- package/server/graphql/resolvers/outbound/find-loadable-release-good.ts +9 -7
- package/server/graphql/resolvers/outbound/load-by-qty.ts +198 -81
- package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +4 -0
- package/server/graphql/resolvers/outbound/undo-load.ts +116 -44
- package/server/graphql/resolvers/route-label/route-label.ts +9 -12
- package/server/graphql/resolvers/zone-worksheet/zone-picking.ts +41 -2
|
@@ -65,97 +65,214 @@ export const loadByQty = {
|
|
|
65
65
|
|
|
66
66
|
const updatedOrderInventories: OrderInventory[] = []
|
|
67
67
|
for (const wsd of updatedWorksheetDetails) {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// Check if this is combined data with multiple order inventory references
|
|
69
|
+
// (from bin scan flow with combineSameProductsWithAllRefs)
|
|
70
|
+
if (wsd.allRelatedOrderInvs && wsd.allRelatedOrderInvs.length > 0) {
|
|
71
|
+
// Combined mode: distribute loadedQty across multiple order inventories (FIFO)
|
|
72
|
+
let remainingLoadQty = wsd.loadedQty
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
for (const ref of wsd.allRelatedOrderInvs) {
|
|
75
|
+
if (remainingLoadQty <= 0) break
|
|
76
|
+
|
|
77
|
+
let orderInventory = orderInventories.find(oi => oi.id === ref.id)
|
|
78
|
+
if (!orderInventory) continue
|
|
79
|
+
|
|
80
|
+
const foundWorksheetDetail = worksheet.worksheetDetails.find(
|
|
81
|
+
foundWsd => foundWsd.targetInventory.id === orderInventory.id
|
|
82
|
+
)
|
|
83
|
+
if (!foundWorksheetDetail) continue
|
|
84
|
+
|
|
85
|
+
// Skip if already loaded or DO generated
|
|
86
|
+
if (orderInventory.status === ORDER_INVENTORY_STATUS.LOADED || orderInventory.deliveryOrderId !== null) {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Calculate how much to load from this order inventory
|
|
91
|
+
const availableQty = orderInventory.pickedQty
|
|
92
|
+
const qtyToLoad = Math.min(remainingLoadQty, availableQty)
|
|
93
|
+
remainingLoadQty -= qtyToLoad
|
|
94
|
+
|
|
95
|
+
if (qtyToLoad === orderInventory.releaseQty) {
|
|
96
|
+
// Full load - mark as LOADED
|
|
97
|
+
orderInventory = {
|
|
98
|
+
...orderInventory,
|
|
99
|
+
status: ORDER_INVENTORY_STATUS.LOADED,
|
|
100
|
+
updater: user,
|
|
101
|
+
updatedAt: new Date()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
|
|
105
|
+
status: WORKSHEET_STATUS.DONE,
|
|
106
|
+
updater: user,
|
|
107
|
+
updatedAt: new Date()
|
|
108
|
+
})
|
|
109
|
+
} else if (qtyToLoad < orderInventory.releaseQty && qtyToLoad > 0) {
|
|
110
|
+
// Partial load - split order inventory
|
|
111
|
+
const remainingOiQty = orderInventory.releaseQty - qtyToLoad
|
|
112
|
+
const newOrderInventory = tx.getRepository(OrderInventory).create({
|
|
113
|
+
...orderInventory,
|
|
114
|
+
id: undefined,
|
|
115
|
+
name: OrderNoGenerator.orderInventory(),
|
|
116
|
+
releaseQty: remainingOiQty,
|
|
117
|
+
releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * remainingOiQty,
|
|
118
|
+
pickedQty: orderInventory.pickedQty - qtyToLoad,
|
|
119
|
+
sortedQty: orderInventory.sortedQty === 0 ? 0 : orderInventory.sortedQty - qtyToLoad,
|
|
120
|
+
status: orderInventory.status,
|
|
121
|
+
updater: user,
|
|
122
|
+
updatedAt: new Date()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await tx.getRepository(OrderInventory).save(newOrderInventory)
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
127
|
+
const newWorksheetDetail = tx.getRepository(WorksheetDetail).create({
|
|
128
|
+
...foundWorksheetDetail,
|
|
129
|
+
id: undefined,
|
|
130
|
+
name: `LOAD-DETAIL-` + uuidv4(),
|
|
131
|
+
worksheet: worksheet,
|
|
132
|
+
targetInventory: newOrderInventory,
|
|
133
|
+
status: WORKSHEET_STATUS.EXECUTING,
|
|
134
|
+
updater: user,
|
|
135
|
+
updatedAt: new Date()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await tx.getRepository(WorksheetDetail).save(newWorksheetDetail)
|
|
139
|
+
|
|
140
|
+
orderInventory = {
|
|
141
|
+
...orderInventory,
|
|
142
|
+
status: ORDER_INVENTORY_STATUS.LOADED,
|
|
143
|
+
releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * qtyToLoad,
|
|
144
|
+
releaseQty: qtyToLoad,
|
|
145
|
+
pickedQty: qtyToLoad,
|
|
146
|
+
sortedQty: qtyToLoad,
|
|
147
|
+
updater: user,
|
|
148
|
+
updatedAt: new Date()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
|
|
152
|
+
status: WORKSHEET_STATUS.DONE,
|
|
153
|
+
updater: user,
|
|
154
|
+
updatedAt: new Date()
|
|
155
|
+
})
|
|
156
|
+
} else {
|
|
157
|
+
// qtyToLoad is 0, skip this order inventory
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const inventory = await tx.getRepository(Inventory).findOne({
|
|
162
|
+
where: { id: orderInventory.inventoryId }
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.LOADING, 0, 0, user, tx)
|
|
166
|
+
updatedOrderInventories.push(orderInventory)
|
|
91
167
|
}
|
|
92
168
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
...orderInventory,
|
|
105
|
-
id: undefined,
|
|
106
|
-
name: OrderNoGenerator.orderInventory(),
|
|
107
|
-
releaseQty: remainingQty,
|
|
108
|
-
releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * remainingQty,
|
|
109
|
-
pickedQty: orderInventory.pickedQty - wsd.loadedQty,
|
|
110
|
-
sortedQty: orderInventory.sortedQty === 0 ? 0 : orderInventory.sortedQty - wsd.loadedQty,
|
|
111
|
-
status: orderInventory.status,
|
|
112
|
-
updater: user,
|
|
113
|
-
updatedAt: new Date()
|
|
114
|
-
})
|
|
169
|
+
// Validate that all requested quantity was loaded
|
|
170
|
+
if (remainingLoadQty > 0) {
|
|
171
|
+
console.warn(`Could not fully distribute loadedQty for SKU ${wsd.productSku}. Remaining: ${remainingLoadQty}`)
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Standard mode: single order inventory reference
|
|
175
|
+
// Support both:
|
|
176
|
+
// 1. Match by worksheetDetail name (when using LOADING_WORKSHEET_V2 query)
|
|
177
|
+
// 2. Match by relatedOrderInv.id (when name is null)
|
|
178
|
+
let foundWorksheetDetail: WorksheetDetail
|
|
179
|
+
let orderInventory: OrderInventory
|
|
115
180
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
name: `LOAD-DETAIL-` + uuidv4(),
|
|
123
|
-
worksheet: worksheet,
|
|
124
|
-
targetInventory: newOrderInventory,
|
|
125
|
-
status: WORKSHEET_STATUS.EXECUTING,
|
|
126
|
-
updater: user,
|
|
127
|
-
updatedAt: new Date()
|
|
128
|
-
})
|
|
181
|
+
if (wsd.name) {
|
|
182
|
+
foundWorksheetDetail = worksheet.worksheetDetails.find(foundWsd => foundWsd.name === wsd.name)
|
|
183
|
+
if (foundWorksheetDetail) {
|
|
184
|
+
orderInventory = orderInventories.find(oi => oi.id === foundWorksheetDetail.targetInventory.id)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
129
187
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
releaseQty: wsd.loadedQty,
|
|
138
|
-
pickedQty: wsd.loadedQty,
|
|
139
|
-
sortedQty: wsd.loadedQty,
|
|
140
|
-
updater: user,
|
|
141
|
-
updatedAt: new Date()
|
|
188
|
+
if (!foundWorksheetDetail && wsd.relatedOrderInv?.id) {
|
|
189
|
+
orderInventory = orderInventories.find(oi => oi.id === wsd.relatedOrderInv.id)
|
|
190
|
+
if (orderInventory) {
|
|
191
|
+
foundWorksheetDetail = worksheet.worksheetDetails.find(
|
|
192
|
+
foundWsd => foundWsd.targetInventory.id === orderInventory.id
|
|
193
|
+
)
|
|
194
|
+
}
|
|
142
195
|
}
|
|
143
196
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
if (!foundWorksheetDetail || !orderInventory) {
|
|
198
|
+
console.warn(`Could not find worksheet detail or order inventory for item: ${JSON.stringify({ name: wsd.name, relatedOrderInvId: wsd.relatedOrderInv?.id, productSku: wsd.productSku })}`)
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (orderInventory.status === ORDER_INVENTORY_STATUS.LOADED || orderInventory.deliveryOrderId !== null) {
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (wsd.loadedQty > orderInventory.pickedQty) {
|
|
207
|
+
throw new Error('Load qty is more than picked qty for sku ' + wsd.productSku)
|
|
208
|
+
} else if (wsd.loadedQty == orderInventory.releaseQty) {
|
|
209
|
+
orderInventory = {
|
|
210
|
+
...orderInventory,
|
|
211
|
+
status: ORDER_INVENTORY_STATUS.LOADED,
|
|
212
|
+
updater: user,
|
|
213
|
+
updatedAt: new Date()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
|
|
217
|
+
status: WORKSHEET_STATUS.DONE,
|
|
218
|
+
updater: user,
|
|
219
|
+
updatedAt: new Date()
|
|
220
|
+
})
|
|
221
|
+
} else if (wsd.loadedQty < orderInventory.releaseQty) {
|
|
222
|
+
const remainingQty = orderInventory.releaseQty - wsd.loadedQty
|
|
223
|
+
const newOrderInventory = tx.getRepository(OrderInventory).create({
|
|
224
|
+
...orderInventory,
|
|
225
|
+
id: undefined,
|
|
226
|
+
name: OrderNoGenerator.orderInventory(),
|
|
227
|
+
releaseQty: remainingQty,
|
|
228
|
+
releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * remainingQty,
|
|
229
|
+
pickedQty: orderInventory.pickedQty - wsd.loadedQty,
|
|
230
|
+
sortedQty: orderInventory.sortedQty === 0 ? 0 : orderInventory.sortedQty - wsd.loadedQty,
|
|
231
|
+
status: orderInventory.status,
|
|
232
|
+
updater: user,
|
|
233
|
+
updatedAt: new Date()
|
|
234
|
+
})
|
|
151
235
|
|
|
152
|
-
|
|
153
|
-
where: { id: orderInventory.inventoryId }
|
|
154
|
-
})
|
|
236
|
+
await tx.getRepository(OrderInventory).save(newOrderInventory)
|
|
155
237
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
const newWorksheetDetail = tx.getRepository(WorksheetDetail).create({
|
|
239
|
+
...foundWorksheetDetail,
|
|
240
|
+
id: undefined,
|
|
241
|
+
name: `LOAD-DETAIL-` + uuidv4(),
|
|
242
|
+
worksheet: worksheet,
|
|
243
|
+
targetInventory: newOrderInventory,
|
|
244
|
+
status: WORKSHEET_STATUS.EXECUTING,
|
|
245
|
+
updater: user,
|
|
246
|
+
updatedAt: new Date()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await tx.getRepository(WorksheetDetail).save(newWorksheetDetail)
|
|
250
|
+
|
|
251
|
+
orderInventory = {
|
|
252
|
+
...orderInventory,
|
|
253
|
+
status: ORDER_INVENTORY_STATUS.LOADED,
|
|
254
|
+
releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * wsd.loadedQty,
|
|
255
|
+
releaseQty: wsd.loadedQty,
|
|
256
|
+
pickedQty: wsd.loadedQty,
|
|
257
|
+
sortedQty: wsd.loadedQty,
|
|
258
|
+
updater: user,
|
|
259
|
+
updatedAt: new Date()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
|
|
263
|
+
status: WORKSHEET_STATUS.DONE,
|
|
264
|
+
updater: user,
|
|
265
|
+
updatedAt: new Date()
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const inventory = await tx.getRepository(Inventory).findOne({
|
|
270
|
+
where: { id: orderInventory.inventoryId }
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.LOADING, 0, 0, user, tx)
|
|
274
|
+
updatedOrderInventories.push(orderInventory)
|
|
275
|
+
}
|
|
159
276
|
}
|
|
160
277
|
|
|
161
278
|
if (updatedOrderInventories.length < 1) {
|
|
@@ -163,6 +163,10 @@ export const loadingWorksheetv2 = {
|
|
|
163
163
|
if (filter === FILTER_TYPES.LOADING) {
|
|
164
164
|
if (shouldFilterBySortedQty(releaseGood.status, disableQc)) {
|
|
165
165
|
qb.andWhere('targetInventory.sortedQty > 0')
|
|
166
|
+
// Exclude LOADED items - they should only appear in LOADED tab
|
|
167
|
+
qb.andWhere('targetInventory.status <> :loadedStatus', {
|
|
168
|
+
loadedStatus: ORDER_INVENTORY_STATUS.LOADED
|
|
169
|
+
})
|
|
166
170
|
} else {
|
|
167
171
|
qb.andWhere('targetInventory.status IN (:...loadingStatus)', {
|
|
168
172
|
loadingStatus: [ORDER_INVENTORY_STATUS.SORTING, ORDER_INVENTORY_STATUS.LOADING]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntityManager } from 'typeorm'
|
|
2
|
-
import { DeliveryOrder,
|
|
1
|
+
import { EntityManager, In } from 'typeorm'
|
|
2
|
+
import { DeliveryOrder, OrderInventory, ORDER_INVENTORY_STATUS } from '@things-factory/sales-base'
|
|
3
3
|
import { Domain } from '@things-factory/shell'
|
|
4
4
|
import { User } from '@things-factory/auth-base'
|
|
5
5
|
import { generateInventoryHistory, Inventory, INVENTORY_TRANSACTION_TYPE } from '@things-factory/warehouse-base'
|
|
@@ -19,63 +19,135 @@ export const undoLoad = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const releaseGood = deliveryOrder.releaseGood
|
|
22
|
+
const doInventories = deliveryOrder.orderInventories
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
if (!doInventories.length) {
|
|
25
|
+
await tx.getRepository(DeliveryOrder).delete(deliveryOrder.id)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// OPTIMIZATION 1: Batch fetch all worksheet details in one query instead of N queries
|
|
30
|
+
const doInventoryIds = doInventories.map(oi => oi.id)
|
|
31
|
+
const worksheetDetails = await tx.getRepository(WorksheetDetail).find({
|
|
32
|
+
where: {
|
|
33
|
+
targetInventory: { id: In(doInventoryIds) },
|
|
34
|
+
type: WORKSHEET_TYPE.LOADING,
|
|
35
|
+
status: WORKSHEET_STATUS.DONE
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Create a map for O(1) lookup of worksheet details by targetInventory.id
|
|
40
|
+
const wsdByTargetId = new Map<string, WorksheetDetail>()
|
|
41
|
+
worksheetDetails.forEach((wsd: WorksheetDetail) => {
|
|
42
|
+
const targetInvId = (wsd as any).targetInventory?.id
|
|
43
|
+
if (targetInvId) {
|
|
44
|
+
wsdByTargetId.set(targetInvId, wsd)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// OPTIMIZATION 2: Create a map for O(1) lookup of previous (split) order inventories
|
|
49
|
+
// Key: inventoryId_productId_productDetailId, Value: OrderInventory with LOADING status
|
|
50
|
+
const previousOiMap = new Map<string, OrderInventory>()
|
|
51
|
+
const doInventoryIdSet = new Set(doInventoryIds)
|
|
52
|
+
releaseGood.orderInventories.forEach(roInventory => {
|
|
53
|
+
if (!doInventoryIdSet.has(roInventory.id) && roInventory.status === ORDER_INVENTORY_STATUS.LOADING) {
|
|
54
|
+
const key = `${roInventory.inventoryId}_${roInventory.productId}_${roInventory.productDetailId}`
|
|
55
|
+
previousOiMap.set(key, roInventory)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Categorize order inventories for batch processing
|
|
60
|
+
const toTerminate: { doInventory: OrderInventory; previousOi: OrderInventory; wsd: WorksheetDetail }[] = []
|
|
61
|
+
const toRevert: { doInventory: OrderInventory; wsd: WorksheetDetail }[] = []
|
|
62
|
+
|
|
63
|
+
for (const doInventory of doInventories) {
|
|
64
|
+
const worksheetDetail = wsdByTargetId.get(doInventory.id)
|
|
65
|
+
const lookupKey = `${doInventory.inventoryId}_${doInventory.productId}_${doInventory.productDetailId}`
|
|
66
|
+
const previousOi = previousOiMap.get(lookupKey)
|
|
36
67
|
|
|
37
68
|
if (previousOi) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
toTerminate.push({ doInventory, previousOi, wsd: worksheetDetail })
|
|
70
|
+
} else {
|
|
71
|
+
toRevert.push({ doInventory, wsd: worksheetDetail })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// OPTIMIZATION 3: Batch update operations
|
|
76
|
+
const now = new Date()
|
|
77
|
+
|
|
78
|
+
// Process terminations (split order inventories)
|
|
79
|
+
if (toTerminate.length > 0) {
|
|
80
|
+
// Update previous OIs with combined quantities - must be done individually due to different values
|
|
81
|
+
await Promise.all(
|
|
82
|
+
toTerminate.map(({ doInventory, previousOi }) =>
|
|
83
|
+
tx.getRepository(OrderInventory).update(previousOi.id, {
|
|
84
|
+
releaseQty: previousOi.releaseQty + doInventory.releaseQty,
|
|
85
|
+
pickedQty: previousOi.pickedQty + doInventory.pickedQty,
|
|
86
|
+
sortedQty: previousOi.sortedQty + doInventory.sortedQty,
|
|
87
|
+
releaseUomValue: previousOi.releaseUomValue + doInventory.releaseUomValue,
|
|
88
|
+
updater: user,
|
|
89
|
+
updatedAt: now
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// Batch terminate the DO inventories
|
|
95
|
+
const terminateIds = toTerminate.map(t => t.doInventory.id)
|
|
96
|
+
await tx.getRepository(OrderInventory).update(
|
|
97
|
+
{ id: In(terminateIds) },
|
|
98
|
+
{
|
|
50
99
|
deliveryOrder: null,
|
|
51
100
|
status: ORDER_INVENTORY_STATUS.TERMINATED,
|
|
52
|
-
updatedAt:
|
|
101
|
+
updatedAt: now,
|
|
53
102
|
updater: user
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Batch delete worksheet details
|
|
107
|
+
const wsdToDeleteIds = toTerminate.filter(t => t.wsd?.id).map(t => t.wsd.id)
|
|
108
|
+
if (wsdToDeleteIds.length > 0) {
|
|
109
|
+
await tx.getRepository(WorksheetDetail).delete({ id: In(wsdToDeleteIds) })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Process reversions (non-split order inventories)
|
|
114
|
+
if (toRevert.length > 0) {
|
|
115
|
+
// Batch update order inventories to LOADING status
|
|
116
|
+
const revertOiIds = toRevert.map(r => r.doInventory.id)
|
|
117
|
+
await tx.getRepository(OrderInventory).update(
|
|
118
|
+
{ id: In(revertOiIds) },
|
|
119
|
+
{
|
|
60
120
|
deliveryOrder: null,
|
|
61
121
|
status: ORDER_INVENTORY_STATUS.LOADING,
|
|
62
122
|
loadedAt: null,
|
|
63
123
|
loadedByUser: null,
|
|
64
124
|
loadedBy: null,
|
|
65
125
|
updater: user
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
status: WORKSHEET_STATUS.EXECUTING,
|
|
69
|
-
updater: user,
|
|
70
|
-
updatedAt: new Date()
|
|
71
|
-
})
|
|
72
|
-
}
|
|
126
|
+
}
|
|
127
|
+
)
|
|
73
128
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
129
|
+
// Batch update worksheet details to EXECUTING status
|
|
130
|
+
const wsdToUpdateIds = toRevert.filter(r => r.wsd?.id).map(r => r.wsd.id)
|
|
131
|
+
if (wsdToUpdateIds.length > 0) {
|
|
132
|
+
await tx.getRepository(WorksheetDetail).update(
|
|
133
|
+
{ id: In(wsdToUpdateIds) },
|
|
134
|
+
{
|
|
135
|
+
status: WORKSHEET_STATUS.EXECUTING,
|
|
136
|
+
updater: user,
|
|
137
|
+
updatedAt: now
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
}
|
|
77
141
|
}
|
|
78
142
|
|
|
143
|
+
// OPTIMIZATION 4: Generate inventory histories in parallel
|
|
144
|
+
await Promise.all(
|
|
145
|
+
doInventories.map(doInventory => {
|
|
146
|
+
const inventory: Inventory = doInventory.inventory
|
|
147
|
+
return generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.UNDO_LOADING, 0, 0, user, tx)
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
|
|
79
151
|
await tx.getRepository(DeliveryOrder).delete(deliveryOrder.id)
|
|
80
152
|
|
|
81
153
|
return true
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { EntityManager, In } from 'typeorm'
|
|
2
2
|
import { Domain } from '@things-factory/biz-base'
|
|
3
|
-
import { WORKSHEET_STATUS } from '@things-factory/worksheet-base'
|
|
4
3
|
import { User } from '@things-factory/auth-base'
|
|
5
4
|
|
|
6
5
|
import { RouteLabelSequenceCounter } from '../../../entities/route-label-sequence-counter'
|
|
7
6
|
import { RouteLabelSequenceLog } from '../../../entities/route-label-sequence-log'
|
|
8
7
|
|
|
8
|
+
// Statuses to exclude from route label printing
|
|
9
|
+
const EXCLUDED_RO_STATUSES = ['DONE', 'PENDING_CANCEL', 'CANCELLED']
|
|
10
|
+
|
|
9
11
|
function ensureSeqField(field: string) {
|
|
10
12
|
if (!['refNo', 'refNo2', 'refNo3'].includes(field)) {
|
|
11
13
|
throw new Error('Invalid sequence field. Allowed: refNo, refNo2, refNo3')
|
|
@@ -43,7 +45,7 @@ export const routeLabelReleaseGoodsInExecutingWorksheet = {
|
|
|
43
45
|
|
|
44
46
|
const rows = await tx.query(
|
|
45
47
|
`
|
|
46
|
-
select
|
|
48
|
+
select
|
|
47
49
|
rg.id as "id",
|
|
48
50
|
rg.name as "name",
|
|
49
51
|
rg.ref_no as "refNo",
|
|
@@ -54,19 +56,14 @@ export const routeLabelReleaseGoodsInExecutingWorksheet = {
|
|
|
54
56
|
rg.store_name as "storeName",
|
|
55
57
|
rg.stop_id as "stopId",
|
|
56
58
|
rg.created_at as "createdAt"
|
|
57
|
-
from
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
where ws.domain_id = $1
|
|
62
|
-
and ws.type = $2
|
|
63
|
-
and ws.status = $3
|
|
64
|
-
and rg.domain_id = $1
|
|
65
|
-
and rg.bizplace_id = $4
|
|
59
|
+
from release_goods rg
|
|
60
|
+
where rg.domain_id = $1
|
|
61
|
+
and rg.bizplace_id = $2
|
|
62
|
+
and rg.status not in ($3, $4, $5)
|
|
66
63
|
${filterRefNo}
|
|
67
64
|
order by rg.created_at desc
|
|
68
65
|
`,
|
|
69
|
-
[domain.id,
|
|
66
|
+
[domain.id, partnerId, ...EXCLUDED_RO_STATUSES]
|
|
70
67
|
)
|
|
71
68
|
|
|
72
69
|
const releaseGoods = rows || []
|
|
@@ -58,9 +58,11 @@ export const zonePicking = {
|
|
|
58
58
|
let worksheetDetails = worksheet.worksheetDetails
|
|
59
59
|
let oiIds = worksheetDetails.map(wsd => wsd.targetInventory.id)
|
|
60
60
|
|
|
61
|
+
// Use pessimistic lock to prevent race conditions with concurrent requests
|
|
61
62
|
const targetInventories: OrderInventory[] = await tx
|
|
62
63
|
.getRepository(OrderInventory)
|
|
63
64
|
.createQueryBuilder('oi')
|
|
65
|
+
.setLock('pessimistic_write')
|
|
64
66
|
.innerJoinAndSelect('oi.inventory', 'i', 'i.id = oi.inventory_id')
|
|
65
67
|
.where('oi.id IN(:...oiIds)', { oiIds: oiIds })
|
|
66
68
|
.andWhere('i.carton_id = :cartonId', { cartonId: cartonId })
|
|
@@ -136,6 +138,10 @@ export const zonePicking = {
|
|
|
136
138
|
oi => oi.productId === product.id && oi.productDetailId === productDetail.id
|
|
137
139
|
)
|
|
138
140
|
|
|
141
|
+
// Track processed OI and inventory IDs to prevent duplicate processing within this transaction
|
|
142
|
+
const processedOiIds: Set<string> = new Set()
|
|
143
|
+
const processedInventoryDeductions: Map<string, number> = new Map()
|
|
144
|
+
|
|
139
145
|
//update all oi
|
|
140
146
|
for (const oi of productOrderInventory) {
|
|
141
147
|
let targetInventory: OrderInventory = oi
|
|
@@ -145,6 +151,12 @@ export const zonePicking = {
|
|
|
145
151
|
const releaseGood: ReleaseGood = targetInventory.releaseGood
|
|
146
152
|
if (pickedQty <= 0) break
|
|
147
153
|
|
|
154
|
+
// Skip if this OI was already processed in this transaction
|
|
155
|
+
if (processedOiIds.has(targetInventory.id)) {
|
|
156
|
+
console.warn(`Skipping duplicate OI processing: ${targetInventory.id}`)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
// validation to prevent picking if the inventory is involved in cycle count
|
|
149
161
|
let inventoryCheckItem: InventoryCheckItem = await tx.getRepository(InventoryCheckItem).findOne({
|
|
150
162
|
where: {
|
|
@@ -173,6 +185,19 @@ export const zonePicking = {
|
|
|
173
185
|
targetReleaseQty > targetPickedQty &&
|
|
174
186
|
pickedQty > 0
|
|
175
187
|
) {
|
|
188
|
+
// Re-verify OI status from database to prevent race condition
|
|
189
|
+
const freshOiStatus = await tx
|
|
190
|
+
.getRepository(OrderInventory)
|
|
191
|
+
.createQueryBuilder('oi')
|
|
192
|
+
.select('oi.status')
|
|
193
|
+
.where('oi.id = :id', { id: targetInventory.id })
|
|
194
|
+
.getRawOne()
|
|
195
|
+
|
|
196
|
+
if (freshOiStatus?.oi_status !== ORDER_INVENTORY_STATUS.PROCESSING) {
|
|
197
|
+
console.warn(`OI ${targetInventory.id} status changed to ${freshOiStatus?.oi_status}, skipping`)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
176
201
|
let remainingAssignedQty: number = pickedQty - (targetReleaseQty - targetPickedQty) // -1, 0, 1
|
|
177
202
|
const gapQtyRemaining: number = targetReleaseQty - targetPickedQty // > 0
|
|
178
203
|
|
|
@@ -194,7 +219,11 @@ export const zonePicking = {
|
|
|
194
219
|
|
|
195
220
|
if (targetInventory.pickedQty == targetReleaseQty) {
|
|
196
221
|
//update oi and wsd to done if pickedQty matches releaseQty
|
|
197
|
-
|
|
222
|
+
// Account for any previous deductions to this inventory in this transaction
|
|
223
|
+
const previousDeduction = processedInventoryDeductions.get(inventory.id) || 0
|
|
224
|
+
const adjustedInventoryQty = inventory.qty - previousDeduction
|
|
225
|
+
const leftQty: number = adjustedInventoryQty - targetInventory.releaseQty
|
|
226
|
+
|
|
198
227
|
if (leftQty < 0) {
|
|
199
228
|
throw new Error(
|
|
200
229
|
wsController.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `quantity can't exceed limitation`)
|
|
@@ -209,6 +238,9 @@ export const zonePicking = {
|
|
|
209
238
|
targetInventory.status = ORDER_INVENTORY_STATUS.PICKED
|
|
210
239
|
await updateOrderInventories(tx, targetInventory)
|
|
211
240
|
|
|
241
|
+
// Mark this OI as processed
|
|
242
|
+
processedOiIds.add(targetInventory.id)
|
|
243
|
+
|
|
212
244
|
//update loading wsd if exist
|
|
213
245
|
let loadingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
|
|
214
246
|
where: { type: WORKSHEET_TYPE.LOADING, targetInventory: targetInventory }
|
|
@@ -242,8 +274,13 @@ export const zonePicking = {
|
|
|
242
274
|
.returning(['qty'])
|
|
243
275
|
.execute()
|
|
244
276
|
|
|
277
|
+
// Track the deduction for this inventory
|
|
278
|
+
processedInventoryDeductions.set(
|
|
279
|
+
inventory.id,
|
|
280
|
+
previousDeduction + targetInventory.releaseQty
|
|
281
|
+
)
|
|
282
|
+
|
|
245
283
|
console.time('inv history')
|
|
246
|
-
//slow
|
|
247
284
|
await generateInventoryHistory(
|
|
248
285
|
inventory,
|
|
249
286
|
releaseGood,
|
|
@@ -275,6 +312,8 @@ export const zonePicking = {
|
|
|
275
312
|
} else {
|
|
276
313
|
//updates oi if pickedQty != releaseQty
|
|
277
314
|
await updateOrderInventories(tx, targetInventory)
|
|
315
|
+
// Mark this OI as processed even for partial picks
|
|
316
|
+
processedOiIds.add(targetInventory.id)
|
|
278
317
|
}
|
|
279
318
|
//deduct pickedQty for the next oi
|
|
280
319
|
pickedQty = pickedQty - (targetReleaseQty - targetPickedQty)
|