@things-factory/operato-wms 4.3.752 → 4.3.755
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/inbound/unload-product.js +30 -33
- package/client/pages/outbound/loading-product.js +2 -3
- package/client/pages/outbound/loading-v2/loading-execution-base.js +568 -56
- package/client/pages/outbound/loading-v2/loading-execution.js +258 -8
- package/client/pages/outbound/loading-v2/loading.js +82 -12
- package/client/pages/outbound/picking-product.js +11 -6
- package/client/pages/outbound/picking-v2/picking-execution-base.js +8 -8
- package/client/pages/outbound/picking-v2/picking-execution.js +75 -0
- package/client/pages/outbound/route-label-history-dialog.js +253 -0
- package/client/pages/outbound/route-label-popup.js +637 -131
- package/client/pages/outbound/show-ro-list-popup.js +8 -4
- package/client/pages/outbound/single-outbound-worksheet.js +55 -6
- package/client/pages/outbound/sorting-product.js +71 -25
- package/client/pages/outbound/zone-worksheet.js +62 -3
- package/dist-server/entities/index.js +9 -0
- package/dist-server/entities/index.js.map +1 -0
- package/dist-server/entities/route-label-sequence-counter.js +75 -0
- package/dist-server/entities/route-label-sequence-counter.js.map +1 -0
- package/dist-server/entities/route-label-sequence-log.js +74 -0
- package/dist-server/entities/route-label-sequence-log.js.map +1 -0
- package/dist-server/graphql/resolvers/index.js +3 -0
- package/dist-server/graphql/resolvers/index.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/complete-load.js +40 -3
- package/dist-server/graphql/resolvers/outbound/complete-load.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +50 -11
- 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 +90 -48
- package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +15 -7
- package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
- package/dist-server/graphql/resolvers/outbound/sort-item.js +95 -16
- package/dist-server/graphql/resolvers/outbound/sort-item.js.map +1 -1
- package/dist-server/graphql/resolvers/route-label/index.js +7 -0
- package/dist-server/graphql/resolvers/route-label/index.js.map +1 -0
- package/dist-server/graphql/resolvers/route-label/route-label.js +220 -0
- package/dist-server/graphql/resolvers/route-label/route-label.js.map +1 -0
- package/dist-server/graphql/resolvers/zone-worksheet/index.js.map +1 -1
- package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js +123 -27
- package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js.map +1 -1
- package/dist-server/graphql/resolvers/zone-worksheet/zone-worksheet.js.map +1 -1
- package/dist-server/graphql/types/index.js +4 -0
- package/dist-server/graphql/types/index.js.map +1 -1
- package/dist-server/graphql/types/outbound/index.js +7 -1
- package/dist-server/graphql/types/outbound/index.js.map +1 -1
- package/dist-server/graphql/types/route-label/index.js +14 -0
- package/dist-server/graphql/types/route-label/index.js.map +1 -0
- package/dist-server/graphql/types/route-label/route-label-sequence.js +53 -0
- package/dist-server/graphql/types/route-label/route-label-sequence.js.map +1 -0
- package/dist-server/index.js +5 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/validators/setting-validators.js +519 -0
- package/dist-server/validators/setting-validators.js.map +1 -0
- package/helps/operato-wms/outbound/picking-product.en.md +156 -158
- package/package.json +50 -50
- package/server/entities/index.ts +6 -0
- package/server/entities/route-label-sequence-counter.ts +56 -0
- package/server/entities/route-label-sequence-log.ts +47 -0
- package/server/graphql/resolvers/index.ts +3 -0
- package/server/graphql/resolvers/outbound/complete-load.ts +42 -1
- package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +56 -13
- package/server/graphql/resolvers/outbound/find-loadable-release-good.ts +120 -57
- package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +9 -2
- package/server/graphql/resolvers/outbound/sort-item.ts +125 -21
- package/server/graphql/resolvers/route-label/index.ts +16 -0
- package/server/graphql/resolvers/route-label/route-label.ts +264 -0
- package/server/graphql/resolvers/zone-worksheet/index.ts +2 -2
- package/server/graphql/resolvers/zone-worksheet/zone-picking.ts +156 -39
- package/server/graphql/resolvers/zone-worksheet/zone-worksheet.ts +1 -2
- package/server/graphql/types/index.ts +4 -0
- package/server/graphql/types/outbound/index.ts +7 -1
- package/server/graphql/types/route-label/index.ts +18 -0
- package/server/graphql/types/route-label/route-label-sequence.ts +50 -0
- package/server/index.ts +7 -1
- package/server/validators/setting-validators.ts +668 -0
- package/translations/en.json +280 -252
- package/translations/ko.json +29 -1
- package/translations/ms.json +28 -0
- package/translations/zh.json +28 -0
|
@@ -1,12 +1,67 @@
|
|
|
1
|
-
import { EntityManager } from 'typeorm'
|
|
2
|
-
import { Brackets } from 'typeorm'
|
|
1
|
+
import { EntityManager, Brackets, IsNull, In, Not } from 'typeorm'
|
|
3
2
|
|
|
4
3
|
import { User } from '@things-factory/auth-base'
|
|
5
4
|
import { Domain } from '@things-factory/shell'
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ORDER_INVENTORY_STATUS,
|
|
7
|
+
OrderInventory,
|
|
8
|
+
OrderNoGenerator,
|
|
9
|
+
OrderTote,
|
|
10
|
+
OrderToteItem,
|
|
11
|
+
ReleaseGood
|
|
12
|
+
} from '@things-factory/sales-base'
|
|
13
|
+
import { Tote, TOTE_STATUS } from '@things-factory/warehouse-base'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculates the available quantity that can still be sorted for an order inventory
|
|
17
|
+
*/
|
|
18
|
+
function getAvailableQty(oi: OrderInventory): number {
|
|
19
|
+
const available = Math.min(oi.releaseQty, oi.pickedQty) - oi.sortedQty
|
|
20
|
+
return available > 0 ? available : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates or updates an order tote item with the given quantity
|
|
25
|
+
*/
|
|
26
|
+
async function upsertOrderToteItem(
|
|
27
|
+
tx: EntityManager,
|
|
28
|
+
domain: Domain,
|
|
29
|
+
user: User,
|
|
30
|
+
orderTote: OrderTote,
|
|
31
|
+
orderInventory: OrderInventory,
|
|
32
|
+
qtyToAdd: number
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const orderToteItemRepo = tx.getRepository(OrderToteItem)
|
|
35
|
+
const existingItem = (await orderToteItemRepo.findOne({
|
|
36
|
+
where: {
|
|
37
|
+
domain,
|
|
38
|
+
orderTote,
|
|
39
|
+
orderInventory,
|
|
40
|
+
orderProduct: orderInventory.orderProduct
|
|
41
|
+
}
|
|
42
|
+
})) as OrderToteItem | null
|
|
43
|
+
|
|
44
|
+
if (!existingItem) {
|
|
45
|
+
await orderToteItemRepo.save({
|
|
46
|
+
domain,
|
|
47
|
+
name: OrderNoGenerator.orderToteItem(),
|
|
48
|
+
orderTote,
|
|
49
|
+
orderInventory,
|
|
50
|
+
orderProduct: orderInventory.orderProduct,
|
|
51
|
+
qty: qtyToAdd,
|
|
52
|
+
updater: user
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
await orderToteItemRepo.update(existingItem.id, {
|
|
56
|
+
qty: Number(existingItem.qty ?? 0) + qtyToAdd,
|
|
57
|
+
updater: user,
|
|
58
|
+
updatedAt: new Date()
|
|
59
|
+
} as any)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
7
62
|
|
|
8
63
|
export const sortItem = {
|
|
9
|
-
async sortItem(_: any, { releaseGoodNo, productBarcode, qty, worksheetDetails }, context: any) {
|
|
64
|
+
async sortItem(_: any, { releaseGoodNo, productBarcode, qty, worksheetDetails, toteNo }, context: any) {
|
|
10
65
|
const { domain, user, tx }: { domain: Domain; user: User; tx: EntityManager } = context.state
|
|
11
66
|
|
|
12
67
|
// Single query to get order inventories based on either releaseGood name or binLocation
|
|
@@ -15,21 +70,22 @@ export const sortItem = {
|
|
|
15
70
|
.createQueryBuilder('orderInventory')
|
|
16
71
|
.innerJoinAndSelect('orderInventory.productDetail', 'productDetail')
|
|
17
72
|
.innerJoinAndSelect('productDetail.productBarcodes', 'productBarcode')
|
|
73
|
+
.leftJoinAndSelect('orderInventory.orderProduct', 'orderProduct')
|
|
18
74
|
.leftJoinAndSelect('orderInventory.releaseGood', 'releaseGood')
|
|
75
|
+
.leftJoinAndSelect('releaseGood.bizplace', 'bizplace')
|
|
19
76
|
.leftJoinAndSelect('orderInventory.binLocation', 'binLocation')
|
|
20
77
|
.where('orderInventory.domain_id = :domainId', { domainId: domain.id })
|
|
21
78
|
.andWhere('orderInventory.status IN (:...statuses)', {
|
|
22
|
-
statuses: [
|
|
23
|
-
ORDER_INVENTORY_STATUS.SORTING,
|
|
24
|
-
ORDER_INVENTORY_STATUS.LOADING
|
|
25
|
-
]
|
|
79
|
+
statuses: [ORDER_INVENTORY_STATUS.SORTING, ORDER_INVENTORY_STATUS.LOADING]
|
|
26
80
|
})
|
|
27
81
|
.andWhere('productBarcode.gtin = :gtin', { gtin: productBarcode })
|
|
28
|
-
.andWhere(
|
|
29
|
-
qb
|
|
30
|
-
.where('orderInventory.sortedQty < orderInventory.pickedQty')
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
.andWhere(
|
|
83
|
+
new Brackets(qb => {
|
|
84
|
+
qb.where('orderInventory.sortedQty < orderInventory.pickedQty').andWhere(
|
|
85
|
+
'orderInventory.sortedQty < orderInventory.releaseQty'
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
)
|
|
33
89
|
.andWhere(
|
|
34
90
|
new Brackets(qb => {
|
|
35
91
|
qb.where('releaseGood.name = :releaseGoodNo', { releaseGoodNo }).orWhere(
|
|
@@ -42,11 +98,55 @@ export const sortItem = {
|
|
|
42
98
|
|
|
43
99
|
if (!orderInventories.length) throw new Error('No matching product barcode found')
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
101
|
+
let resolvedReleaseGood: ReleaseGood | null = null
|
|
102
|
+
let resolvedOrderTote: OrderTote | null = null
|
|
103
|
+
|
|
104
|
+
if (toteNo) {
|
|
105
|
+
// Ensure we can identify a single release order when scanning by BIN
|
|
106
|
+
const uniqueReleaseGoodIds = [...new Set(orderInventories.map(oi => oi.releaseGoodId).filter(Boolean))]
|
|
107
|
+
if (uniqueReleaseGoodIds.length > 1) {
|
|
108
|
+
throw new Error('Multiple release orders detected. Please select a release order before tote scanning.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolvedReleaseGood = await tx.getRepository(ReleaseGood).findOne({
|
|
112
|
+
where: { domain, id: uniqueReleaseGoodIds[0] },
|
|
113
|
+
relations: ['bizplace']
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (!resolvedReleaseGood) {
|
|
117
|
+
throw new Error('Release good not found for tote scanning')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolvedOrderTote = await tx.getRepository(OrderTote).findOne({
|
|
121
|
+
where: { domain, name: toteNo, releaseGood: resolvedReleaseGood }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (!resolvedOrderTote) {
|
|
125
|
+
// Query for valid totes (NOT dispatched or damaged)
|
|
126
|
+
const foundTote: Tote = await tx.getRepository(Tote).findOne({
|
|
127
|
+
where: {
|
|
128
|
+
bizplace: resolvedReleaseGood.bizplace,
|
|
129
|
+
name: toteNo,
|
|
130
|
+
deletedAt: IsNull(),
|
|
131
|
+
status: Not(In([TOTE_STATUS.DISPATCHED, TOTE_STATUS.DAMAGED]))
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
resolvedOrderTote = await tx.getRepository(OrderTote).save({
|
|
136
|
+
name: toteNo,
|
|
137
|
+
domain,
|
|
138
|
+
releaseGood: resolvedReleaseGood,
|
|
139
|
+
tote: foundTote ?? null,
|
|
140
|
+
creator: user,
|
|
141
|
+
updater: user
|
|
142
|
+
})
|
|
143
|
+
} else if (resolvedOrderTote.closedDate) {
|
|
144
|
+
throw new Error('Tote has been sealed, please try another tote!')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Calculate total available quantity using helper function
|
|
149
|
+
const totalAvailableQty = orderInventories.reduce((sum, oi) => sum + getAvailableQty(oi), 0)
|
|
50
150
|
|
|
51
151
|
if (qty > totalAvailableQty) {
|
|
52
152
|
throw new Error(`Sort qty (${qty}) exceeds total available quantity (${totalAvailableQty})`)
|
|
@@ -57,20 +157,24 @@ export const sortItem = {
|
|
|
57
157
|
for (const orderInventory of orderInventories) {
|
|
58
158
|
if (remainingQty <= 0) break
|
|
59
159
|
|
|
60
|
-
const availableQty =
|
|
160
|
+
const availableQty = getAvailableQty(orderInventory)
|
|
61
161
|
if (availableQty <= 0) continue
|
|
62
162
|
|
|
63
163
|
const qtyToAdd = Math.min(remainingQty, availableQty)
|
|
64
|
-
const newSortQty = orderInventory.sortedQty + qtyToAdd
|
|
65
164
|
|
|
66
165
|
// Update the order inventory
|
|
67
166
|
await tx.getRepository(OrderInventory).update(orderInventory.id, {
|
|
68
|
-
sortedQty:
|
|
167
|
+
sortedQty: orderInventory.sortedQty + qtyToAdd,
|
|
69
168
|
status: ORDER_INVENTORY_STATUS.LOADING,
|
|
70
169
|
updatedAt: new Date(),
|
|
71
170
|
updater: user
|
|
72
171
|
})
|
|
73
172
|
|
|
173
|
+
// Create / update order tote items to reflect tote allocation during QC
|
|
174
|
+
if (resolvedOrderTote) {
|
|
175
|
+
await upsertOrderToteItem(tx, domain, user, resolvedOrderTote, orderInventory, qtyToAdd)
|
|
176
|
+
}
|
|
177
|
+
|
|
74
178
|
remainingQty -= qtyToAdd
|
|
75
179
|
}
|
|
76
180
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reserveRouteLabelSequence,
|
|
3
|
+
routeLabelReleaseGoodsInExecutingWorksheet,
|
|
4
|
+
routeLabelSequenceLogs,
|
|
5
|
+
routeLabelSequenceStatuses
|
|
6
|
+
} from './route-label'
|
|
7
|
+
|
|
8
|
+
export const Query = {
|
|
9
|
+
...routeLabelReleaseGoodsInExecutingWorksheet,
|
|
10
|
+
...routeLabelSequenceStatuses,
|
|
11
|
+
...routeLabelSequenceLogs
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Mutation = {
|
|
15
|
+
...reserveRouteLabelSequence
|
|
16
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { EntityManager, In } from 'typeorm'
|
|
2
|
+
import { Domain } from '@things-factory/biz-base'
|
|
3
|
+
import { WORKSHEET_STATUS } from '@things-factory/worksheet-base'
|
|
4
|
+
import { User } from '@things-factory/auth-base'
|
|
5
|
+
|
|
6
|
+
import { RouteLabelSequenceCounter } from '../../../entities/route-label-sequence-counter'
|
|
7
|
+
import { RouteLabelSequenceLog } from '../../../entities/route-label-sequence-log'
|
|
8
|
+
|
|
9
|
+
function ensureSeqField(field: string) {
|
|
10
|
+
if (!['refNo', 'refNo2', 'refNo3'].includes(field)) {
|
|
11
|
+
throw new Error('Invalid sequence field. Allowed: refNo, refNo2, refNo3')
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseNonNegativeInt(value: any): number {
|
|
16
|
+
const n = parseInt(value ?? '0', 10)
|
|
17
|
+
return Number.isFinite(n) ? Math.max(0, n) : 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const routeLabelReleaseGoodsInExecutingWorksheet = {
|
|
21
|
+
async routeLabelReleaseGoodsInExecutingWorksheet(_: any, { partnerId, refNo, worksheetType, seqField }, context: any) {
|
|
22
|
+
const { domain, tx }: { domain: Domain; tx: EntityManager } = context.state
|
|
23
|
+
|
|
24
|
+
let filterRefNo = ''
|
|
25
|
+
if (refNo) {
|
|
26
|
+
const removeSymbol = refNo
|
|
27
|
+
.split('')
|
|
28
|
+
.filter(res => res !== '%')
|
|
29
|
+
.join('')
|
|
30
|
+
|
|
31
|
+
const filterInfos = removeSymbol
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.split(',')
|
|
34
|
+
.map(value => "'%" + value.trim().replace(/'/g, "''") + "%'")
|
|
35
|
+
.join(',')
|
|
36
|
+
|
|
37
|
+
filterRefNo = ` and (
|
|
38
|
+
lower(rg.ref_no) like any(array[${filterInfos}])
|
|
39
|
+
or lower(rg.ref_no_2) like any(array[${filterInfos}])
|
|
40
|
+
or lower(rg.ref_no_3) like any(array[${filterInfos}])
|
|
41
|
+
)`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rows = await tx.query(
|
|
45
|
+
`
|
|
46
|
+
select distinct
|
|
47
|
+
rg.id as "id",
|
|
48
|
+
rg.name as "name",
|
|
49
|
+
rg.ref_no as "refNo",
|
|
50
|
+
rg.ref_no_2 as "refNo2",
|
|
51
|
+
rg.ref_no_3 as "refNo3",
|
|
52
|
+
rg.route_id as "routeId",
|
|
53
|
+
rg.store_id as "storeId",
|
|
54
|
+
rg.store_name as "storeName",
|
|
55
|
+
rg.stop_id as "stopId",
|
|
56
|
+
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
|
|
66
|
+
${filterRefNo}
|
|
67
|
+
order by rg.created_at desc
|
|
68
|
+
`,
|
|
69
|
+
[domain.id, worksheetType, WORKSHEET_STATUS.EXECUTING, partnerId]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const releaseGoods = rows || []
|
|
73
|
+
|
|
74
|
+
// If seqField is provided, fetch and merge sequence data
|
|
75
|
+
if (seqField && ['refNo', 'refNo2', 'refNo3'].includes(seqField) && releaseGoods.length > 0) {
|
|
76
|
+
const releaseGoodIds = releaseGoods.map((rg: any) => rg.id)
|
|
77
|
+
|
|
78
|
+
// Fetch lastSeq from counters
|
|
79
|
+
const lastSeqById = new Map<string, number>()
|
|
80
|
+
const counterRepo = tx.getRepository(RouteLabelSequenceCounter)
|
|
81
|
+
const counters = await counterRepo.find({
|
|
82
|
+
where: { domain, releaseGoodId: In(releaseGoodIds), field: seqField }
|
|
83
|
+
})
|
|
84
|
+
for (const c of counters) {
|
|
85
|
+
lastSeqById.set(c.releaseGoodId, parseNonNegativeInt(c.lastSeq))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fetch lastPrintedBy from logs (most recent per releaseGood)
|
|
89
|
+
const lastPrintedById = new Map<string, string>()
|
|
90
|
+
const logRepo = tx.getRepository(RouteLabelSequenceLog)
|
|
91
|
+
const logs = await logRepo.find({
|
|
92
|
+
where: { domain, releaseGoodId: In(releaseGoodIds), field: seqField },
|
|
93
|
+
relations: ['creator'],
|
|
94
|
+
order: { createdAt: 'DESC' },
|
|
95
|
+
take: 500
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
for (const log of logs) {
|
|
99
|
+
if (!lastPrintedById.has(log.releaseGoodId) && log.creator) {
|
|
100
|
+
lastPrintedById.set(log.releaseGoodId, log.creator.name || '')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Merge sequence data into release goods
|
|
105
|
+
return releaseGoods.map((rg: any) => ({
|
|
106
|
+
...rg,
|
|
107
|
+
lastSeq: lastSeqById.get(rg.id) || 0,
|
|
108
|
+
lastPrintedBy: lastPrintedById.get(rg.id) || null
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return releaseGoods
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const routeLabelSequenceStatuses = {
|
|
117
|
+
async routeLabelSequenceStatuses(_: any, { releaseGoodIds, field }, context: any) {
|
|
118
|
+
const { domain, tx }: { domain: Domain; tx: EntityManager } = context.state
|
|
119
|
+
ensureSeqField(field)
|
|
120
|
+
|
|
121
|
+
const lastSeqById = new Map<string, number>()
|
|
122
|
+
const counterRepo = tx.getRepository(RouteLabelSequenceCounter)
|
|
123
|
+
const counters = await counterRepo.find({
|
|
124
|
+
where: { domain, releaseGoodId: In(releaseGoodIds), field }
|
|
125
|
+
})
|
|
126
|
+
for (const c of counters) {
|
|
127
|
+
lastSeqById.set(c.releaseGoodId, parseNonNegativeInt(c.lastSeq))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const histById = new Map<string, string[]>()
|
|
131
|
+
const lastPrintedById = new Map<string, string>()
|
|
132
|
+
const logRepo = tx.getRepository(RouteLabelSequenceLog)
|
|
133
|
+
const logs = await logRepo.find({
|
|
134
|
+
where: { domain, releaseGoodId: In(releaseGoodIds), field },
|
|
135
|
+
relations: ['creator'],
|
|
136
|
+
order: { createdAt: 'DESC' },
|
|
137
|
+
take: 500
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
for (const log of logs) {
|
|
141
|
+
const list = histById.get(log.releaseGoodId) || []
|
|
142
|
+
if (list.length >= 5) continue
|
|
143
|
+
list.push(`${log.from}-${log.to}`)
|
|
144
|
+
histById.set(log.releaseGoodId, list)
|
|
145
|
+
|
|
146
|
+
// Store the creator name from the most recent log entry (first one due to DESC order)
|
|
147
|
+
if (!lastPrintedById.has(log.releaseGoodId) && log.creator) {
|
|
148
|
+
lastPrintedById.set(log.releaseGoodId, log.creator.name || '')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return releaseGoodIds.map((releaseGoodId: string) => {
|
|
153
|
+
return {
|
|
154
|
+
releaseGoodId,
|
|
155
|
+
field,
|
|
156
|
+
lastSeq: lastSeqById.get(releaseGoodId) || 0,
|
|
157
|
+
history: histById.get(releaseGoodId) || [],
|
|
158
|
+
lastPrintedBy: lastPrintedById.get(releaseGoodId) || null
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const reserveRouteLabelSequence = {
|
|
165
|
+
async reserveRouteLabelSequence(_: any, { releaseGoodId, field, qty }, context: any) {
|
|
166
|
+
const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state
|
|
167
|
+
ensureSeqField(field)
|
|
168
|
+
|
|
169
|
+
const q = parseNonNegativeInt(qty)
|
|
170
|
+
if (q <= 0) {
|
|
171
|
+
throw new Error('Print qty must be greater than 0')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const counterRepo = tx.getRepository(RouteLabelSequenceCounter)
|
|
175
|
+
const logRepo = tx.getRepository(RouteLabelSequenceLog)
|
|
176
|
+
|
|
177
|
+
// Lock counter row to avoid duplicated sequences under concurrent prints.
|
|
178
|
+
let counter = await counterRepo
|
|
179
|
+
.createQueryBuilder('c')
|
|
180
|
+
.setLock('pessimistic_write')
|
|
181
|
+
.where('c.domain = :domainId and c.releaseGoodId = :releaseGoodId and c.field = :field', {
|
|
182
|
+
domainId: domain.id,
|
|
183
|
+
releaseGoodId,
|
|
184
|
+
field
|
|
185
|
+
})
|
|
186
|
+
.getOne()
|
|
187
|
+
|
|
188
|
+
if (!counter) {
|
|
189
|
+
try {
|
|
190
|
+
await counterRepo.save({
|
|
191
|
+
domain,
|
|
192
|
+
releaseGoodId,
|
|
193
|
+
field,
|
|
194
|
+
lastSeq: 0,
|
|
195
|
+
creator: user,
|
|
196
|
+
updater: user
|
|
197
|
+
})
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// ignore: concurrent insert can violate unique index
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
counter = await counterRepo
|
|
203
|
+
.createQueryBuilder('c')
|
|
204
|
+
.setLock('pessimistic_write')
|
|
205
|
+
.where('c.domain = :domainId and c.releaseGoodId = :releaseGoodId and c.field = :field', {
|
|
206
|
+
domainId: domain.id,
|
|
207
|
+
releaseGoodId,
|
|
208
|
+
field
|
|
209
|
+
})
|
|
210
|
+
.getOne()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!counter) {
|
|
214
|
+
throw new Error('Failed to initialize route label sequence counter')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const last = parseNonNegativeInt(counter.lastSeq)
|
|
218
|
+
const from = last + 1
|
|
219
|
+
const to = last + q
|
|
220
|
+
|
|
221
|
+
await counterRepo.update(counter.id, { lastSeq: to, updater: user, updatedAt: new Date() })
|
|
222
|
+
await logRepo.save({ domain, releaseGoodId, field, from, to, qty: q, creator: user })
|
|
223
|
+
|
|
224
|
+
return { releaseGoodId, field, from, to }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const routeLabelSequenceLogs = {
|
|
229
|
+
async routeLabelSequenceLogs(_: any, { releaseGoodId, field }, context: any) {
|
|
230
|
+
const { domain, tx }: { domain: Domain; tx: EntityManager } = context.state
|
|
231
|
+
ensureSeqField(field)
|
|
232
|
+
|
|
233
|
+
const rows =
|
|
234
|
+
(await tx.query(
|
|
235
|
+
`
|
|
236
|
+
select
|
|
237
|
+
l.id as "id",
|
|
238
|
+
l.release_good_id as "releaseGoodId",
|
|
239
|
+
l.field as "field",
|
|
240
|
+
l."from" as "from",
|
|
241
|
+
l."to" as "to",
|
|
242
|
+
l.qty as "qty",
|
|
243
|
+
l.created_at as "createdAt",
|
|
244
|
+
u.name as "creatorName"
|
|
245
|
+
from route_label_sequence_logs l
|
|
246
|
+
left join users u on u.id = l.creator_id
|
|
247
|
+
where l.domain_id = $1
|
|
248
|
+
and l.release_good_id = $2
|
|
249
|
+
and l.field = $3
|
|
250
|
+
order by l.created_at desc
|
|
251
|
+
limit 200
|
|
252
|
+
`,
|
|
253
|
+
[domain.id, releaseGoodId, field]
|
|
254
|
+
)) || []
|
|
255
|
+
|
|
256
|
+
return rows.map((row: any) => {
|
|
257
|
+
const createdAt = row.createdAt ? new Date(row.createdAt) : null
|
|
258
|
+
return {
|
|
259
|
+
...row,
|
|
260
|
+
createdAt: createdAt ? createdAt.toISOString() : null
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { zonePickingWorksheet, zoneWorksheetOverview } from './zone-worksheet'
|
|
2
|
-
import { generateWorksheetZonePicking
|
|
2
|
+
import { generateWorksheetZonePicking } from './generate-zone-worksheet'
|
|
3
3
|
import { activateBulkWorksheetZonePicking, activateWorksheetZonePicking } from './activate-zone-worksheet'
|
|
4
4
|
import { zonePicking } from './zone-picking'
|
|
5
5
|
import { assignSorter } from './assign-sorter'
|
|
@@ -7,7 +7,7 @@ import { getZonePickingTask } from './get-zone-picking-task'
|
|
|
7
7
|
|
|
8
8
|
export const Query = {
|
|
9
9
|
...zoneWorksheetOverview,
|
|
10
|
-
...zonePickingWorksheet
|
|
10
|
+
...zonePickingWorksheet
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export const Mutation = {
|