@things-factory/worksheet-base 4.3.824 → 4.3.827
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/picking-worksheet-controller.js +53 -84
- package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
- package/dist-server/entities/warehouse-bizplace-onhand-inventory.js +2 -2
- package/dist-server/graphql/resolvers/worksheet/confirm-cancellation-release-order.js +1 -1
- package/dist-server/graphql/resolvers/worksheet/confirm-cancellation-release-order.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/inventories-by-pallet.js +1 -1
- package/dist-server/graphql/resolvers/worksheet/inventories-by-pallet.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 +4 -4
- package/server/controllers/outbound/picking-worksheet-controller.ts +77 -94
- package/server/entities/warehouse-bizplace-onhand-inventory.ts +2 -2
- package/server/graphql/resolvers/worksheet/confirm-cancellation-release-order.ts +1 -1
- package/server/graphql/resolvers/worksheet/inventories-by-pallet.ts +1 -1
- package/server/graphql/resolvers/worksheet/recommend-putaway-location.ts +110 -4
|
@@ -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
|
+
|