@things-factory/worksheet-base 4.3.823 → 4.3.825
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/controllers/outbound/loading-worksheet-controller.js +7 -0
- package/dist-server/controllers/outbound/loading-worksheet-controller.js.map +1 -1
- package/dist-server/controllers/outbound/picking-worksheet-controller.js +58 -58
- package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
- package/dist-server/controllers/outbound/sorting-worksheet-controller.js +6 -6
- package/dist-server/controllers/outbound/sorting-worksheet-controller.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/picking/complete-picking.js +0 -2
- package/dist-server/graphql/resolvers/worksheet/picking/complete-picking.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/picking/fetch-and-assign-picking-task.js +17 -6
- package/dist-server/graphql/resolvers/worksheet/picking/fetch-and-assign-picking-task.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/recommend-putaway-location.js +94 -4
- package/dist-server/graphql/resolvers/worksheet/recommend-putaway-location.js.map +1 -1
- package/package.json +12 -12
- package/server/controllers/outbound/loading-worksheet-controller.ts +9 -0
- package/server/controllers/outbound/picking-worksheet-controller.ts +69 -69
- package/server/controllers/outbound/sorting-worksheet-controller.ts +7 -7
- package/server/graphql/resolvers/worksheet/picking/complete-picking.ts +0 -2
- package/server/graphql/resolvers/worksheet/picking/fetch-and-assign-picking-task.ts +17 -6
- package/server/graphql/resolvers/worksheet/recommend-putaway-location.ts +110 -4
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { EntityManager, Equal,
|
|
1
|
+
import { EntityManager, Equal, getManager, In, IsNull, Not } from 'typeorm'
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid'
|
|
3
3
|
|
|
4
4
|
import { ApplicationType, User } from '@things-factory/auth-base'
|
|
5
5
|
import { Bizplace } from '@things-factory/biz-base'
|
|
6
6
|
import { logger } from '@things-factory/env'
|
|
7
7
|
import { generateId } from '@things-factory/id-rule-base'
|
|
8
|
-
import { Powrup } from '@things-factory/integration-powrup'
|
|
8
|
+
import { notifyPowrupOrderStatus, Powrup } from '@things-factory/integration-powrup'
|
|
9
9
|
import { Sellercraft, SellercraftStatus } from '@things-factory/integration-sellercraft'
|
|
10
10
|
import { Product, ProductDetail } from '@things-factory/product-base'
|
|
11
11
|
import {
|
|
@@ -436,6 +436,9 @@ export class PickingWorksheetController extends VasWorksheetController {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.PickingStarted)
|
|
439
|
+
if (releaseGood.source === ApplicationType.POWRUP) {
|
|
440
|
+
notifyPowrupOrderStatus(releaseGood, 'PICKING', worksheet.startedAt)
|
|
441
|
+
}
|
|
439
442
|
|
|
440
443
|
if (newReleaseGood) {
|
|
441
444
|
worksheet.releaseGood = newReleaseGood
|
|
@@ -722,6 +725,9 @@ export class PickingWorksheetController extends VasWorksheetController {
|
|
|
722
725
|
|
|
723
726
|
for (const releaseGood of releaseGoods) {
|
|
724
727
|
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.PickingStarted)
|
|
728
|
+
if (releaseGood.source === ApplicationType.POWRUP) {
|
|
729
|
+
notifyPowrupOrderStatus(releaseGood, 'PICKING', worksheet.startedAt)
|
|
730
|
+
}
|
|
725
731
|
}
|
|
726
732
|
|
|
727
733
|
return worksheet
|
|
@@ -1108,9 +1114,6 @@ export class PickingWorksheetController extends VasWorksheetController {
|
|
|
1108
1114
|
await this.toteScanning(toteNo, targetProduct, targetInventory, pickedQty, releaseGood, bizplace)
|
|
1109
1115
|
}
|
|
1110
1116
|
|
|
1111
|
-
// temporarily override with separate logic to handle transaction to speed up function
|
|
1112
|
-
// await this.updatePickingTransaction(releaseGood, targetInventory, worksheetDetail, inventory, pickedQty)
|
|
1113
|
-
|
|
1114
1117
|
const releaseQty: number = targetInventory.releaseQty
|
|
1115
1118
|
|
|
1116
1119
|
targetInventory.pickedQty = (targetInventory?.pickedQty || 0) + pickedQty
|
|
@@ -1141,78 +1144,72 @@ export class PickingWorksheetController extends VasWorksheetController {
|
|
|
1141
1144
|
.execute()
|
|
1142
1145
|
|
|
1143
1146
|
if (oiUpdateResult.affected > 0 && targetInventory.pickedQty == releaseQty) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
.where('id = :id', { id: worksheetDetailInfos.worksheetDetailId })
|
|
1157
|
-
.execute()
|
|
1147
|
+
//update worksheet details only when line item picking complete
|
|
1148
|
+
await this.trxMgr
|
|
1149
|
+
.getRepository(WorksheetDetail)
|
|
1150
|
+
.createQueryBuilder()
|
|
1151
|
+
.update(WorksheetDetail)
|
|
1152
|
+
.set({
|
|
1153
|
+
status: WORKSHEET_STATUS.DONE,
|
|
1154
|
+
updater: this.user,
|
|
1155
|
+
updatedAt: new Date()
|
|
1156
|
+
})
|
|
1157
|
+
.where('id = :id', { id: worksheetDetailInfos.worksheetDetailId })
|
|
1158
|
+
.execute()
|
|
1158
1159
|
|
|
1159
|
-
|
|
1160
|
+
let releaseUomValue = Math.trunc((pickedUomValue / pickedQty) * releaseQty * 1000) / 1000
|
|
1160
1161
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1162
|
+
let updateInvObj = {
|
|
1163
|
+
qty: () => `"qty" - :deductQty::numeric`,
|
|
1164
|
+
lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
|
|
1165
|
+
uomValue: () => `"uom_value" - :deductUomValue::numeric`,
|
|
1166
|
+
lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
|
|
1167
|
+
status: () =>
|
|
1168
|
+
`case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
|
|
1169
|
+
updater: this.user,
|
|
1170
|
+
updatedAt: new Date()
|
|
1171
|
+
}
|
|
1171
1172
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1173
|
+
const invUpdateResult = await this.trxMgr
|
|
1174
|
+
.getRepository(Inventory)
|
|
1175
|
+
.createQueryBuilder()
|
|
1176
|
+
.update(Inventory)
|
|
1177
|
+
.set(updateInvObj)
|
|
1178
|
+
.setParameter('deductQty', releaseQty)
|
|
1179
|
+
.setParameter('deductUomValue', releaseUomValue)
|
|
1180
|
+
.where('id = :id AND qty >= :deductQty', {
|
|
1181
|
+
id: worksheetDetailInfos.inventoryId,
|
|
1182
|
+
deductQty: releaseQty
|
|
1183
|
+
})
|
|
1184
|
+
.returning(['qty'])
|
|
1185
|
+
.execute()
|
|
1185
1186
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1187
|
+
if (invUpdateResult.affected === 0) {
|
|
1188
|
+
throw new Error(`Insufficient inventory quantity to complete picking`)
|
|
1189
|
+
}
|
|
1189
1190
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1191
|
+
await generateInventoryHistory(
|
|
1192
|
+
inventory,
|
|
1193
|
+
releaseGood,
|
|
1194
|
+
INVENTORY_TRANSACTION_TYPE.PICKING,
|
|
1195
|
+
-releaseQty,
|
|
1196
|
+
-releaseUomValue,
|
|
1197
|
+
this.user,
|
|
1198
|
+
this.trxMgr
|
|
1199
|
+
)
|
|
1199
1200
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1201
|
+
let inventoryItems: InventoryItem[] = await this.trxMgr
|
|
1202
|
+
.getRepository(InventoryItem)
|
|
1203
|
+
.find({ where: { outboundOrderId: worksheetDetailInfos.releaseGoodId } })
|
|
1203
1204
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1205
|
+
if (inventoryItems.length > 0) {
|
|
1206
|
+
inventoryItems.forEach((itm: InventoryItem) => {
|
|
1207
|
+
itm.status = INVENTORY_STATUS.PICKED
|
|
1208
|
+
itm.updater = this.user
|
|
1209
|
+
})
|
|
1209
1210
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
throw error
|
|
1214
|
-
}
|
|
1215
|
-
})
|
|
1211
|
+
await this.trxMgr.getRepository(InventoryItem).save(inventoryItems)
|
|
1212
|
+
}
|
|
1216
1213
|
}
|
|
1217
1214
|
|
|
1218
1215
|
let worksheetDetail = await this.trxMgr
|
|
@@ -3128,6 +3125,9 @@ export class PickingWorksheetController extends VasWorksheetController {
|
|
|
3128
3125
|
}
|
|
3129
3126
|
|
|
3130
3127
|
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.PickingStarted)
|
|
3128
|
+
if (releaseGood.source === ApplicationType.POWRUP) {
|
|
3129
|
+
notifyPowrupOrderStatus(releaseGood, 'PICKING', worksheet.startedAt)
|
|
3130
|
+
}
|
|
3131
3131
|
|
|
3132
3132
|
worksheet.releaseGood = newReleaseGood ? newReleaseGood : releaseGood
|
|
3133
3133
|
updatedWs.push(worksheet)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { In, IsNull } from 'typeorm'
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid'
|
|
3
|
+
import { ApplicationType } from '@things-factory/auth-base'
|
|
3
4
|
import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
|
|
5
|
+
import { notifyPowrupOrderStatus } from '@things-factory/integration-powrup'
|
|
4
6
|
import { Product, ProductBarcode, ProductDetail } from '@things-factory/product-base'
|
|
5
7
|
import {
|
|
6
8
|
ORDER_INVENTORY_STATUS,
|
|
@@ -143,6 +145,11 @@ export class SortingWorksheetController extends VasWorksheetController {
|
|
|
143
145
|
})
|
|
144
146
|
|
|
145
147
|
await this.trxMgr.getRepository(WorksheetDetail).insert(loadingWorksheetDetails)
|
|
148
|
+
|
|
149
|
+
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.LoadingStarted)
|
|
150
|
+
if (releaseGood.source === ApplicationType.POWRUP) {
|
|
151
|
+
notifyPowrupOrderStatus(releaseGood, 'LOADING')
|
|
152
|
+
}
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
return savedSortingWorksheet
|
|
@@ -700,13 +707,6 @@ export class SortingWorksheetController extends VasWorksheetController {
|
|
|
700
707
|
worksheet.updater = this.user
|
|
701
708
|
worksheet = await this.trxMgr.getRepository(Worksheet).save(worksheet)
|
|
702
709
|
|
|
703
|
-
const releaseGoods: ReleaseGood[] = await this.trxMgr
|
|
704
|
-
.getRepository(ReleaseGood)
|
|
705
|
-
.find({ where: { id: In(releaseGoodIds) }, relations: ['domain', 'bizplace'] })
|
|
706
|
-
|
|
707
|
-
for (let releaseGood of releaseGoods) {
|
|
708
|
-
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.LoadingStarted)
|
|
709
|
-
}
|
|
710
710
|
|
|
711
711
|
return worksheet
|
|
712
712
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { EntityManager, getManager, In } from 'typeorm'
|
|
2
|
-
import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
|
|
3
2
|
import { ApplicationType, User } from '@things-factory/auth-base'
|
|
4
3
|
import { Bizplace, ContactPoint, getMyBizplace } from '@things-factory/biz-base'
|
|
5
4
|
import { logger } from '@things-factory/env'
|
|
@@ -411,7 +410,6 @@ export async function completePicking(
|
|
|
411
410
|
|
|
412
411
|
const loadingWorksheetDetails: WorksheetDetail[] = loadingWorksheet.worksheetDetails
|
|
413
412
|
await loadingWSCtrl.activateLoading(loadingWorksheet.name, loadingWorksheetDetails)
|
|
414
|
-
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.LoadingStarted)
|
|
415
413
|
} else {
|
|
416
414
|
const loadingWSCtrl: LoadingWorksheetController = new LoadingWorksheetController(tx, domain, user)
|
|
417
415
|
let loadingWorksheet: Worksheet = await loadingWSCtrl.updateLoadingWorksheet(
|
|
@@ -56,6 +56,15 @@ export const fetchAndAssignPickingTaskResolver = {
|
|
|
56
56
|
|
|
57
57
|
if (!worksheet) {
|
|
58
58
|
// update and return updated worksheet id
|
|
59
|
+
// FOR UPDATE OF ws SKIP LOCKED: under concurrent clicks, two callers
|
|
60
|
+
// used to SELECT the same row and the second UPDATE would re-evaluate,
|
|
61
|
+
// find assignee_id no longer NULL, update zero rows, and the user
|
|
62
|
+
// would see a confusing "no suitable worksheet found" error even
|
|
63
|
+
// though other unassigned rows existed. SKIP LOCKED makes concurrent
|
|
64
|
+
// callers pick different rows. `OF ws` scopes the row lock to the
|
|
65
|
+
// worksheets table only (not the joined release_goods rows). Inner
|
|
66
|
+
// transaction is independent so the row lock is released as soon as
|
|
67
|
+
// the UPDATE commits.
|
|
59
68
|
let updatedWorksheets = await getConnection().transaction(async (tx2: EntityManager) => {
|
|
60
69
|
let x = await tx2.getRepository(Worksheet).query(
|
|
61
70
|
`
|
|
@@ -65,23 +74,25 @@ export const fetchAndAssignPickingTaskResolver = {
|
|
|
65
74
|
FROM (
|
|
66
75
|
SELECT ws.id, ws.name, ws.status, ws.release_good_id FROM worksheets ws
|
|
67
76
|
INNER JOIN release_goods rg ON rg.id = ws.release_good_id
|
|
68
|
-
WHERE
|
|
69
|
-
ws.domain_id = $1 AND
|
|
77
|
+
WHERE
|
|
78
|
+
ws.domain_id = $1 AND
|
|
70
79
|
ws.type = 'PICKING'
|
|
71
|
-
AND ws.status IN ('EXECUTING', 'DEACTIVATED')
|
|
80
|
+
AND ws.status IN ('EXECUTING', 'DEACTIVATED')
|
|
72
81
|
AND ws.assignee_id IS NULL
|
|
73
82
|
/*
|
|
74
83
|
(condition is commented as current status in oi not in use)
|
|
75
84
|
AND NOT EXISTS (
|
|
76
85
|
SELECT count(id) AS "totalPS"
|
|
77
|
-
FROM order_inventories oi
|
|
86
|
+
FROM order_inventories oi
|
|
78
87
|
WHERE oi.release_good_id = ws.release_good_id AND
|
|
79
88
|
oi.status = 'PENDING_SPLIT'
|
|
80
|
-
GROUP BY oi.release_good_id
|
|
89
|
+
GROUP BY oi.release_good_id
|
|
81
90
|
HAVING count(id) > 0
|
|
82
91
|
)
|
|
83
92
|
*/
|
|
84
|
-
ORDER BY rg.release_date, rg.created_at
|
|
93
|
+
ORDER BY rg.release_date, rg.created_at
|
|
94
|
+
LIMIT 1
|
|
95
|
+
FOR UPDATE OF ws SKIP LOCKED
|
|
85
96
|
) src where src.id = ws.id
|
|
86
97
|
RETURNING ws.id;
|
|
87
98
|
`,
|
|
@@ -3,12 +3,15 @@ import { getRepository, In } from 'typeorm'
|
|
|
3
3
|
import { ArrivalNotice, ReturnOrder } from '@things-factory/sales-base'
|
|
4
4
|
import { Setting } from '@things-factory/setting-base'
|
|
5
5
|
import { Domain } from '@things-factory/shell'
|
|
6
|
-
import { Inventory, Location, LOCATION_STATUS, LOCATION_TYPE, Warehouse } from '@things-factory/warehouse-base'
|
|
6
|
+
import { Inventory, Location, LOCATION_STATUS, LOCATION_TYPE, Warehouse, calcVolumeInM3, buildProductDetailLabel } from '@things-factory/warehouse-base'
|
|
7
7
|
|
|
8
8
|
import { WORKSHEET_STATUS, WORKSHEET_TYPE } from '../../../constants'
|
|
9
9
|
import { Worksheet, WorksheetDetail } from '../../../entities'
|
|
10
10
|
import { isInventoryExpiring } from '../../../utils/inventory-util'
|
|
11
11
|
|
|
12
|
+
// 10% headroom required when storing — accounts for practical height gaps in shelving
|
|
13
|
+
const HEIGHT_GAP_FACTOR = 0.9 // ONLY FOR RECOMMENDATION LEVEL 4
|
|
14
|
+
|
|
12
15
|
export const recommendPutawayLocationResolver = {
|
|
13
16
|
async recommendPutawayLocation(
|
|
14
17
|
_: void,
|
|
@@ -26,7 +29,8 @@ export const recommendPutawayLocationResolver = {
|
|
|
26
29
|
'worksheet.returnOrder',
|
|
27
30
|
'targetInventory',
|
|
28
31
|
'targetInventory.inventory',
|
|
29
|
-
'targetInventory.inventory.product'
|
|
32
|
+
'targetInventory.inventory.product',
|
|
33
|
+
'targetInventory.inventory.productDetail'
|
|
30
34
|
]
|
|
31
35
|
}
|
|
32
36
|
)
|
|
@@ -87,6 +91,10 @@ export const recommendPutawayLocationResolver = {
|
|
|
87
91
|
recommendedLocations = await recommendLocationLevel3(domain, targetWarehouse, optCnt, inventory)
|
|
88
92
|
break
|
|
89
93
|
|
|
94
|
+
case 'level 4':
|
|
95
|
+
recommendedLocations = await recommendLocationLevel4(domain, targetWarehouse, optCnt, inventory)
|
|
96
|
+
break
|
|
97
|
+
|
|
90
98
|
default:
|
|
91
99
|
recommendedLocations = await recommendLocationLevel1(domain, targetWarehouse, optCnt)
|
|
92
100
|
break
|
|
@@ -404,7 +412,7 @@ export async function recommendLocationLevel3(
|
|
|
404
412
|
|
|
405
413
|
// ---------------- EMPTY fallback (same type, ordered by rule) ----------------
|
|
406
414
|
if (recommended.length < optCnt) {
|
|
407
|
-
const
|
|
415
|
+
const emptyLocationsAll = await getRepository(Location).find({
|
|
408
416
|
where: {
|
|
409
417
|
domain,
|
|
410
418
|
warehouse,
|
|
@@ -415,9 +423,107 @@ export async function recommendLocationLevel3(
|
|
|
415
423
|
take: optCnt - recommended.length
|
|
416
424
|
})
|
|
417
425
|
|
|
418
|
-
recommended.push(...
|
|
426
|
+
recommended.push(...emptyLocationsAll)
|
|
419
427
|
}
|
|
420
428
|
}
|
|
421
429
|
|
|
422
430
|
return recommended.slice(0, optCnt)
|
|
423
431
|
}
|
|
432
|
+
|
|
433
|
+
export async function recommendLocationLevel4(
|
|
434
|
+
domain: Domain,
|
|
435
|
+
warehouse: Warehouse,
|
|
436
|
+
optCnt: number,
|
|
437
|
+
inventory: Inventory
|
|
438
|
+
): Promise<Location[]> {
|
|
439
|
+
const allowedTypesInOrder: LOCATION_TYPE[] = [LOCATION_TYPE.STORAGE, LOCATION_TYPE.SHELF, LOCATION_TYPE.FLOOR]
|
|
440
|
+
|
|
441
|
+
// --- Step 1: Check all EMPTY locations have volume ---
|
|
442
|
+
const emptyLocations: Location[] = await getRepository(Location).find({
|
|
443
|
+
where: {
|
|
444
|
+
domain,
|
|
445
|
+
warehouse,
|
|
446
|
+
status: LOCATION_STATUS.EMPTY,
|
|
447
|
+
type: In(allowedTypesInOrder)
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const missingVolume = emptyLocations.some((loc: Location) => !loc.volume)
|
|
452
|
+
if (missingVolume) {
|
|
453
|
+
throw new Error('Please update the Location Master to include volume calculation')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// --- Step 2: Check product dimensions and length unit ---
|
|
457
|
+
const pd = inventory.productDetail
|
|
458
|
+
if (!pd?.lengthUnit || (pd?.volume == null && (!pd?.width || !pd?.depth || !pd?.height))) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
'Missing product dimensions or length unit. Please provide width, depth, height, and length unit for correct volume calculation'
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const incomingDetail = buildProductDetailLabel(pd)
|
|
465
|
+
const incomingLabel = `SKU: ${inventory.product?.sku} ${incomingDetail}`.trim()
|
|
466
|
+
const incomingVolume: number = calcVolumeInM3(pd, incomingLabel) * (inventory.qty ?? 1)
|
|
467
|
+
|
|
468
|
+
// --- Step 3: rule-for-storing-product ---
|
|
469
|
+
const orderSetting: Setting | undefined = await getRepository(Setting).findOne({
|
|
470
|
+
where: { domain, name: 'rule-for-storing-product' }
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
let sortLocation: Record<string, 'ASC' | 'DESC'> = {}
|
|
474
|
+
if (orderSetting?.value) {
|
|
475
|
+
try {
|
|
476
|
+
sortLocation = JSON.parse(orderSetting.value)
|
|
477
|
+
} catch {
|
|
478
|
+
// ignore malformed setting
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// --- Step 4: Check if all empty locations having same volume ---
|
|
483
|
+
const allSameVolume =
|
|
484
|
+
emptyLocations.length > 0 && emptyLocations.every((loc: Location) => loc.volume === emptyLocations[0].volume)
|
|
485
|
+
|
|
486
|
+
if (allSameVolume) {
|
|
487
|
+
const usableVolume = emptyLocations[0].volume * HEIGHT_GAP_FACTOR
|
|
488
|
+
if (incomingVolume > usableVolume) return [] // return early to not recommend any
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const recommended: Location[] = []
|
|
492
|
+
|
|
493
|
+
for (const type of allowedTypesInOrder) {
|
|
494
|
+
if (recommended.length >= optCnt) break
|
|
495
|
+
|
|
496
|
+
const candidates: Location[] = emptyLocations.filter((loc: Location) => loc.type === type)
|
|
497
|
+
if (candidates.length === 0) continue
|
|
498
|
+
|
|
499
|
+
if (allSameVolume) {
|
|
500
|
+
const sortKeys = Object.keys(sortLocation)
|
|
501
|
+
const sorted = candidates.sort((a: Location, b: Location) => {
|
|
502
|
+
for (const key of sortKeys) {
|
|
503
|
+
const dir = sortLocation[key] === 'DESC' ? -1 : 1
|
|
504
|
+
if (a[key] < b[key]) return -1 * dir
|
|
505
|
+
if (a[key] > b[key]) return 1 * dir
|
|
506
|
+
}
|
|
507
|
+
return 0
|
|
508
|
+
})
|
|
509
|
+
recommended.push(...sorted.slice(0, optCnt - recommended.length))
|
|
510
|
+
if (recommended.length >= optCnt) break
|
|
511
|
+
} else {
|
|
512
|
+
// Sort by least remaining space ASC (usable = volume * 0.9, 10% height gap)
|
|
513
|
+
const sorted = candidates
|
|
514
|
+
.map((loc: Location) => {
|
|
515
|
+
const usableVolume = loc.volume * HEIGHT_GAP_FACTOR
|
|
516
|
+
const remaining = usableVolume - incomingVolume
|
|
517
|
+
return { loc, remaining }
|
|
518
|
+
})
|
|
519
|
+
.filter(({ remaining }) => remaining >= 0)
|
|
520
|
+
.sort((a, b) => a.remaining - b.remaining)
|
|
521
|
+
.map(({ loc }) => loc)
|
|
522
|
+
|
|
523
|
+
recommended.push(...sorted.slice(0, optCnt - recommended.length))
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return recommended.slice(0, optCnt)
|
|
528
|
+
}
|
|
529
|
+
|