@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.
Files changed (24) hide show
  1. package/client/pages/outbound/loading-v2/delivery-order-info.js +38 -3
  2. package/client/pages/outbound/loading-v2/loading-execution-base.js +132 -17
  3. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +15 -0
  4. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js.map +1 -1
  5. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js +9 -8
  6. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js.map +1 -1
  7. package/dist-server/graphql/resolvers/outbound/load-by-qty.js +115 -45
  8. package/dist-server/graphql/resolvers/outbound/load-by-qty.js.map +1 -1
  9. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +4 -0
  10. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
  11. package/dist-server/graphql/resolvers/outbound/undo-load.js +94 -42
  12. package/dist-server/graphql/resolvers/outbound/undo-load.js.map +1 -1
  13. package/dist-server/graphql/resolvers/route-label/route-label.js +8 -12
  14. package/dist-server/graphql/resolvers/route-label/route-label.js.map +1 -1
  15. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js +31 -2
  16. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js.map +1 -1
  17. package/package.json +4 -4
  18. package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +15 -0
  19. package/server/graphql/resolvers/outbound/find-loadable-release-good.ts +9 -7
  20. package/server/graphql/resolvers/outbound/load-by-qty.ts +198 -81
  21. package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +4 -0
  22. package/server/graphql/resolvers/outbound/undo-load.ts +116 -44
  23. package/server/graphql/resolvers/route-label/route-label.ts +9 -12
  24. 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
- //find the respective wsd and oi to update
69
- const foundWorksheetDetail: WorksheetDetail = worksheet.worksheetDetails.find(
70
- foundWsd => foundWsd.name === wsd.name
71
- )
72
- let orderInventory = orderInventories.find(oi => oi.id === foundWorksheetDetail.targetInventory.id)
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
- //validate oi status and do is not yet generated
75
- if (orderInventory.status === ORDER_INVENTORY_STATUS.LOADED || orderInventory.deliveryOrderId !== null) {
76
- continue
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
- // compare with sort qty
80
- if (wsd.loadedQty > orderInventory.pickedQty) {
81
- throw new Error('Load qty is more than picked qty for sku ' + wsd.productSku)
82
- } else if (wsd.loadedQty == orderInventory.releaseQty) {
83
- //compare load qty to release, if order is partially picked, has to split oi
84
-
85
- //update oi
86
- orderInventory = {
87
- ...orderInventory,
88
- status: ORDER_INVENTORY_STATUS.LOADED,
89
- updater: user,
90
- updatedAt: new Date()
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
- //update wsd
94
- await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
95
- status: WORKSHEET_STATUS.DONE,
96
- updater: user,
97
- updatedAt: new Date()
98
- })
99
- } else if (wsd.loadedQty < orderInventory.releaseQty) {
100
- //split oi
101
- //create new order inventory with remaining quantity
102
- const remainingQty = orderInventory.releaseQty - wsd.loadedQty
103
- const newOrderInventory = tx.getRepository(OrderInventory).create({
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
- await tx.getRepository(OrderInventory).save(newOrderInventory)
117
-
118
- //create new worksheet detail for the new order inventory
119
- const newWorksheetDetail = tx.getRepository(WorksheetDetail).create({
120
- ...foundWorksheetDetail,
121
- id: undefined,
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
- await tx.getRepository(WorksheetDetail).save(newWorksheetDetail)
131
-
132
- //update current order inventory to loaded with loaded quantity
133
- orderInventory = {
134
- ...orderInventory,
135
- status: ORDER_INVENTORY_STATUS.LOADED,
136
- releaseUomValue: (orderInventory.releaseUomValue / orderInventory.releaseQty) * wsd.loadedQty,
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
- //update current wsd
145
- await tx.getRepository(WorksheetDetail).update(foundWorksheetDetail.id, {
146
- status: WORKSHEET_STATUS.DONE,
147
- updater: user,
148
- updatedAt: new Date()
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
- const inventory = await tx.getRepository(Inventory).findOne({
153
- where: { id: orderInventory.inventoryId }
154
- })
236
+ await tx.getRepository(OrderInventory).save(newOrderInventory)
155
237
 
156
- // generate inventory history
157
- await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.LOADING, 0, 0, user, tx)
158
- updatedOrderInventories.push(orderInventory)
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, ReleaseGood, OrderInventory, ORDER_INVENTORY_STATUS } from '@things-factory/sales-base'
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
- for (const doInventory of deliveryOrder.orderInventories) {
24
- const worksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
25
- where: { targetInventory: doInventory, type: WORKSHEET_TYPE.LOADING, status: WORKSHEET_STATUS.DONE }
26
- })
27
- //find if the oi is splitted and not yet loaded
28
- const previousOi: OrderInventory = releaseGood.orderInventories.find(
29
- roInventory =>
30
- roInventory.id !== doInventory.id &&
31
- roInventory.inventoryId === doInventory.inventoryId &&
32
- roInventory.productId === doInventory.productId &&
33
- roInventory.productDetailId === doInventory.productDetailId &&
34
- roInventory.status === ORDER_INVENTORY_STATUS.LOADING
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
- //combine previously splitted oi
39
- await tx.getRepository(OrderInventory).update(previousOi.id, {
40
- releaseQty: previousOi.releaseQty + doInventory.releaseQty,
41
- pickedQty: previousOi.pickedQty + doInventory.pickedQty,
42
- sortedQty: previousOi.sortedQty + doInventory.sortedQty,
43
- releaseUomValue: previousOi.releaseUomValue + doInventory.releaseUomValue,
44
- updater: user,
45
- updatedAt: new Date()
46
- })
47
-
48
- //terminate splitted oi
49
- await tx.getRepository(OrderInventory).update(doInventory.id, {
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: new Date(),
101
+ updatedAt: now,
53
102
  updater: user
54
- })
55
- //delete wsd
56
- await tx.getRepository(WorksheetDetail).delete({ id: worksheetDetail.id, status: WORKSHEET_STATUS.DONE })
57
- } else {
58
- //undo oi and wsd
59
- await tx.getRepository(OrderInventory).update(doInventory.id, {
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
- await tx.getRepository(WorksheetDetail).update(worksheetDetail.id, {
68
- status: WORKSHEET_STATUS.EXECUTING,
69
- updater: user,
70
- updatedAt: new Date()
71
- })
72
- }
126
+ }
127
+ )
73
128
 
74
- //generate inventory history
75
- let inventory: Inventory = doInventory.inventory
76
- await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.UNDO_LOADING, 0, 0, this.user)
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 distinct
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 worksheets ws
58
- inner join worksheet_details wsd on wsd.worksheet_id = ws.id
59
- inner join order_inventories oi on oi.id = wsd.target_inventory_id
60
- inner join release_goods rg on rg.id = oi.release_good_id
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, worksheetType, WORKSHEET_STATUS.EXECUTING, partnerId]
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
- const leftQty: number = inventory.qty - targetInventory.releaseQty
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)