@things-factory/worksheet-base 4.3.753 → 4.3.756
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 +97 -27
- package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
- package/dist-server/controllers/outbound/sorting-worksheet-controller.js +117 -24
- package/dist-server/controllers/outbound/sorting-worksheet-controller.js.map +1 -1
- package/dist-server/controllers/render-ro-do.js +324 -96
- package/dist-server/controllers/render-ro-do.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.js +244 -123
- package/dist-server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/index.js +1 -1
- package/dist-server/graphql/resolvers/worksheet/index.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/loading/index.js +3 -1
- package/dist-server/graphql/resolvers/worksheet/loading/index.js.map +1 -1
- package/dist-server/graphql/resolvers/worksheet/loading/validate-qc-seals.js +79 -0
- package/dist-server/graphql/resolvers/worksheet/loading/validate-qc-seals.js.map +1 -0
- package/dist-server/graphql/types/worksheet/index.js +5 -1
- package/dist-server/graphql/types/worksheet/index.js.map +1 -1
- package/dist-server/graphql/types/worksheet/validate-qc-seals-result.js +12 -0
- package/dist-server/graphql/types/worksheet/validate-qc-seals-result.js.map +1 -0
- package/dist-server/graphql/types/worksheet/worksheet-info.js +1 -0
- package/dist-server/graphql/types/worksheet/worksheet-info.js.map +1 -1
- package/package.json +21 -21
- package/server/controllers/outbound/picking-worksheet-controller.ts +105 -31
- package/server/controllers/outbound/sorting-worksheet-controller.ts +137 -25
- package/server/controllers/render-ro-do.ts +378 -136
- package/server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.ts +305 -128
- package/server/graphql/resolvers/worksheet/index.ts +3 -2
- package/server/graphql/resolvers/worksheet/loading/index.ts +5 -0
- package/server/graphql/resolvers/worksheet/loading/validate-qc-seals.ts +91 -0
- package/server/graphql/types/worksheet/index.ts +5 -1
- package/server/graphql/types/worksheet/validate-qc-seals-result.ts +9 -0
- package/server/graphql/types/worksheet/worksheet-info.ts +1 -0
|
@@ -8,153 +8,330 @@ import {
|
|
|
8
8
|
} from '@things-factory/sales-base'
|
|
9
9
|
import { Domain } from '@things-factory/shell'
|
|
10
10
|
import { User } from '@things-factory/auth-base'
|
|
11
|
-
import { Location } from '@things-factory/warehouse-base'
|
|
12
11
|
import { SortingWorksheetController } from '../../../controllers/'
|
|
13
12
|
import { PartnerSetting, Setting } from '@things-factory/setting-base'
|
|
14
13
|
|
|
15
14
|
import { WORKSHEET_STATUS, WORKSHEET_TYPE } from '../../../constants'
|
|
16
15
|
import { Worksheet as WorksheetEntity, WorksheetDetail as WorksheetDetailEntity } from '../../../entities'
|
|
17
16
|
|
|
17
|
+
// Simple in-memory cache for settings with TTL
|
|
18
|
+
const settingsCache = new Map<string, { value: Setting | null; expiry: number }>()
|
|
19
|
+
const SETTINGS_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get setting with caching to avoid repeated database queries
|
|
23
|
+
*/
|
|
24
|
+
async function getCachedSetting(
|
|
25
|
+
tx: EntityManager,
|
|
26
|
+
domain: Domain,
|
|
27
|
+
category: string,
|
|
28
|
+
name: string
|
|
29
|
+
): Promise<Setting | null> {
|
|
30
|
+
const cacheKey = `${domain.id}:${category}:${name}`
|
|
31
|
+
const cached = settingsCache.get(cacheKey)
|
|
32
|
+
|
|
33
|
+
if (cached && cached.expiry > Date.now()) {
|
|
34
|
+
return cached.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const setting = await tx.getRepository(Setting).findOne({
|
|
38
|
+
where: { domain, category, name }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
settingsCache.set(cacheKey, { value: setting || null, expiry: Date.now() + SETTINGS_CACHE_TTL })
|
|
42
|
+
return setting || null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find worksheet by release good name - returns task info if found
|
|
47
|
+
*/
|
|
48
|
+
async function findWorksheetByReleaseGoodName(
|
|
49
|
+
tx: EntityManager,
|
|
50
|
+
domain: Domain,
|
|
51
|
+
taskNo: string
|
|
52
|
+
): Promise<{ taskNo: string; selectedReleaseGood: string; status: string } | null> {
|
|
53
|
+
const result = await tx
|
|
54
|
+
.getRepository(WorksheetEntity)
|
|
55
|
+
.createQueryBuilder('ws')
|
|
56
|
+
.select('ws.task_no', 'taskNo')
|
|
57
|
+
.addSelect('ws.status', 'wsStatus')
|
|
58
|
+
.addSelect('rg.name', 'releaseGoodName')
|
|
59
|
+
.addSelect('rg.status', 'releaseGoodStatus')
|
|
60
|
+
.innerJoin('worksheet_details', 'wd', `ws.id = wd.worksheet_id`)
|
|
61
|
+
.innerJoin('order_inventories', 'oi', `wd.target_inventory_id = oi.id AND wd."type" = 'SORTING'`)
|
|
62
|
+
.innerJoin('release_goods', 'rg', `rg.id = oi.release_good_id`)
|
|
63
|
+
.where('rg.domain_id = :domainId', { domainId: domain.id })
|
|
64
|
+
.andWhere('rg.name = :name', { name: taskNo })
|
|
65
|
+
.andWhere('rg.status IN (:...statuses)', {
|
|
66
|
+
statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING, ORDER_STATUS.LOADING, ORDER_STATUS.DONE]
|
|
67
|
+
})
|
|
68
|
+
.andWhere('ws.type = :type', { type: WORKSHEET_TYPE.SORTING })
|
|
69
|
+
.getRawOne()
|
|
70
|
+
|
|
71
|
+
if (!result) return null
|
|
72
|
+
|
|
73
|
+
if (result.releaseGoodStatus === 'LOADING' || result.releaseGoodStatus === 'DONE') {
|
|
74
|
+
throw new Error(`Release Good already sorted`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
taskNo: result.taskNo,
|
|
79
|
+
selectedReleaseGood: result.releaseGoodName,
|
|
80
|
+
status: result.wsStatus
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find worksheet by taskNo directly - optimized to only load needed relations
|
|
86
|
+
*/
|
|
87
|
+
async function findWorksheetByTaskNo(
|
|
88
|
+
tx: EntityManager,
|
|
89
|
+
domain: Domain,
|
|
90
|
+
taskNo: string,
|
|
91
|
+
loadWorksheetDetails: boolean = false
|
|
92
|
+
): Promise<WorksheetEntity | null> {
|
|
93
|
+
const relations = loadWorksheetDetails ? ['bizplace', 'bizplace.domain', 'worksheetDetails'] : ['bizplace', 'bizplace.domain']
|
|
94
|
+
|
|
95
|
+
return tx.getRepository(WorksheetEntity).findOne({
|
|
96
|
+
where: { taskNo, type: WORKSHEET_TYPE.SORTING, domain },
|
|
97
|
+
relations
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find worksheet by bin location - combines location lookup and worksheet query
|
|
103
|
+
* Returns both taskNo and worksheet status in a single optimized query
|
|
104
|
+
*/
|
|
105
|
+
async function findWorksheetByBinLocation(
|
|
106
|
+
tx: EntityManager,
|
|
107
|
+
domain: Domain,
|
|
108
|
+
binName: string
|
|
109
|
+
): Promise<{ taskNo: string; worksheetId: string; status: string } | null> {
|
|
110
|
+
// Combined query: location -> order inventory -> worksheet in one go
|
|
111
|
+
const result = await tx
|
|
112
|
+
.getRepository(OrderInventoryEntity)
|
|
113
|
+
.createQueryBuilder('oi')
|
|
114
|
+
.select('ws2.task_no', 'taskNo')
|
|
115
|
+
.addSelect('ws2.id', 'worksheetId')
|
|
116
|
+
.addSelect('ws2.status', 'status')
|
|
117
|
+
.innerJoin('locations', 'loc', 'oi.bin_location_id = loc.id')
|
|
118
|
+
.innerJoin('worksheets', 'ws', `oi.ref_worksheet_id = ws.id AND ws.type = 'BATCH_PICKING'`)
|
|
119
|
+
.innerJoin('worksheets', 'ws2', `ws2.task_no = ws.task_no AND ws2.type = 'SORTING'`)
|
|
120
|
+
.innerJoin('release_goods', 'rg', 'rg.id = oi.release_good_id')
|
|
121
|
+
.where('oi.domain_id = :domainId', { domainId: domain.id })
|
|
122
|
+
.andWhere('loc.domain_id = :domainId', { domainId: domain.id })
|
|
123
|
+
.andWhere('loc.name = :binName', { binName })
|
|
124
|
+
.andWhere('oi.status IN (:...orderInventoryStatus)', {
|
|
125
|
+
orderInventoryStatus: [ORDER_INVENTORY_STATUS.READY_TO_SORT, ORDER_INVENTORY_STATUS.SORTING]
|
|
126
|
+
})
|
|
127
|
+
.andWhere('rg.status IN (:...statuses)', {
|
|
128
|
+
statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING]
|
|
129
|
+
})
|
|
130
|
+
.andWhere('ws2.status IN (:...worksheetStatuses)', {
|
|
131
|
+
worksheetStatuses: [WORKSHEET_STATUS.DEACTIVATED, WORKSHEET_STATUS.EXECUTING]
|
|
132
|
+
})
|
|
133
|
+
.getRawOne()
|
|
134
|
+
|
|
135
|
+
if (!result) {
|
|
136
|
+
throw new Error(`Bin do not have any batch picking order.`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
taskNo: result.taskNo,
|
|
141
|
+
worksheetId: result.worksheetId,
|
|
142
|
+
status: result.status
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle worksheet activation if needed
|
|
148
|
+
*/
|
|
149
|
+
async function handleWorksheetActivation(
|
|
150
|
+
tx: EntityManager,
|
|
151
|
+
domain: Domain,
|
|
152
|
+
user: User,
|
|
153
|
+
task: WorksheetEntity
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
if (task.status !== 'DEACTIVATED') return
|
|
156
|
+
|
|
157
|
+
const sortingWSCtrl = new SortingWorksheetController(tx, domain, user)
|
|
158
|
+
|
|
159
|
+
// Use cached settings
|
|
160
|
+
const directActivateSetting = await getCachedSetting(
|
|
161
|
+
tx,
|
|
162
|
+
domain,
|
|
163
|
+
'id-rule',
|
|
164
|
+
'enable-direct-activate-sorting-worksheet'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if (!directActivateSetting || directActivateSetting.value?.toLowerCase() !== 'true') {
|
|
168
|
+
throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Only fetch partner setting if main setting is enabled
|
|
172
|
+
const partnerSetting: PartnerSetting | null = await tx.getRepository(PartnerSetting).findOne({
|
|
173
|
+
where: {
|
|
174
|
+
setting: directActivateSetting,
|
|
175
|
+
domain: domain,
|
|
176
|
+
partnerDomain: task.bizplace?.domain
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// If partner setting exists and is false, don't activate
|
|
181
|
+
if (partnerSetting && (partnerSetting.value as string)?.toLowerCase() !== 'true') {
|
|
182
|
+
throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Load worksheet details only when needed for activation
|
|
186
|
+
if (!task.worksheetDetails) {
|
|
187
|
+
const taskWithDetails = await tx.getRepository(WorksheetEntity).findOne({
|
|
188
|
+
where: { id: task.id },
|
|
189
|
+
relations: ['worksheetDetails']
|
|
190
|
+
})
|
|
191
|
+
task.worksheetDetails = taskWithDetails?.worksheetDetails || []
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build and execute release goods query with item counts in a SINGLE query
|
|
199
|
+
* Optimizations:
|
|
200
|
+
* - Removed redundant Domain join (filter directly on ws.domain_id)
|
|
201
|
+
* - Combined release goods + counts into one query using aggregate functions
|
|
202
|
+
*/
|
|
203
|
+
async function getReleaseGoodsWithCounts(
|
|
204
|
+
tx: EntityManager,
|
|
205
|
+
domain: Domain,
|
|
206
|
+
taskNo: string
|
|
207
|
+
): Promise<ReleaseGoodEntity[]> {
|
|
208
|
+
// Single query that gets release goods with their counts using aggregation
|
|
209
|
+
const releaseGoods: any[] = await tx
|
|
210
|
+
.getRepository(WorksheetEntity)
|
|
211
|
+
.createQueryBuilder('ws')
|
|
212
|
+
.select('rg.id', 'id')
|
|
213
|
+
.addSelect('rg.name', 'name')
|
|
214
|
+
.addSelect('rg.refNo', 'refNo')
|
|
215
|
+
.addSelect('rg.refNo2', 'refNo2')
|
|
216
|
+
.addSelect('rg.status', 'status')
|
|
217
|
+
.addSelect('rg.district', 'district')
|
|
218
|
+
.addSelect('rg.attention_company', 'attentionCompany')
|
|
219
|
+
.addSelect('rg.delivery_address_1', 'deliveryAddress1')
|
|
220
|
+
.addSelect('rg.delivery_address_2', 'deliveryAddress2')
|
|
221
|
+
.addSelect('rg.delivery_address_3', 'deliveryAddress3')
|
|
222
|
+
.addSelect('rg.delivery_address_4', 'deliveryAddress4')
|
|
223
|
+
.addSelect('rg.delivery_address_5', 'deliveryAddress5')
|
|
224
|
+
.addSelect('rg.created_at', 'createdAt')
|
|
225
|
+
.addSelect('u.name', 'sortedByUser')
|
|
226
|
+
// Counts computed directly in the same query
|
|
227
|
+
.addSelect('COUNT(oi.id)', 'totalCount')
|
|
228
|
+
.addSelect('SUM(CASE WHEN oi.sorted_qty = oi.release_qty THEN 1 ELSE 0 END)', 'completedCount')
|
|
229
|
+
.innerJoin(WorksheetDetailEntity, 'wsd', 'ws.id = wsd.worksheet_id')
|
|
230
|
+
.innerJoin(OrderInventoryEntity, 'oi', 'oi.id = wsd.target_inventory_id')
|
|
231
|
+
.innerJoin(ReleaseGoodEntity, 'rg', 'rg.id = oi.release_good_id')
|
|
232
|
+
.leftJoin(User, 'u', 'rg.sorted_by_id = u.id')
|
|
233
|
+
// Filter directly on ws.domain_id - no need to join Domain table
|
|
234
|
+
.where('ws.domain_id = :domainId', { domainId: domain.id })
|
|
235
|
+
.andWhere('ws.taskNo = :taskNo', { taskNo })
|
|
236
|
+
.andWhere('ws.type = :worksheetType', { worksheetType: WORKSHEET_TYPE.SORTING })
|
|
237
|
+
.andWhere('ws.status = :worksheetStatus', { worksheetStatus: WORKSHEET_STATUS.EXECUTING })
|
|
238
|
+
.groupBy('rg.id')
|
|
239
|
+
.addGroupBy('rg.name')
|
|
240
|
+
.addGroupBy('rg.refNo')
|
|
241
|
+
.addGroupBy('rg.refNo2')
|
|
242
|
+
.addGroupBy('rg.status')
|
|
243
|
+
.addGroupBy('rg.district')
|
|
244
|
+
.addGroupBy('rg.attention_company')
|
|
245
|
+
.addGroupBy('rg.delivery_address_1')
|
|
246
|
+
.addGroupBy('rg.delivery_address_2')
|
|
247
|
+
.addGroupBy('rg.delivery_address_3')
|
|
248
|
+
.addGroupBy('rg.delivery_address_4')
|
|
249
|
+
.addGroupBy('rg.delivery_address_5')
|
|
250
|
+
.addGroupBy('rg.created_at')
|
|
251
|
+
.addGroupBy('u.name')
|
|
252
|
+
.orderBy('rg.created_at', 'ASC')
|
|
253
|
+
.getRawMany()
|
|
254
|
+
|
|
255
|
+
// Transform results to include itemCounts
|
|
256
|
+
return releaseGoods.map(rg => {
|
|
257
|
+
const total = parseInt(rg.totalCount) || 0
|
|
258
|
+
const completed = parseInt(rg.completedCount) || 0
|
|
259
|
+
return {
|
|
260
|
+
id: rg.id,
|
|
261
|
+
name: rg.name,
|
|
262
|
+
refNo: rg.refNo,
|
|
263
|
+
refNo2: rg.refNo2,
|
|
264
|
+
status: rg.status,
|
|
265
|
+
district: rg.district,
|
|
266
|
+
attentionCompany: rg.attentionCompany,
|
|
267
|
+
deliveryAddress1: rg.deliveryAddress1,
|
|
268
|
+
deliveryAddress2: rg.deliveryAddress2,
|
|
269
|
+
deliveryAddress3: rg.deliveryAddress3,
|
|
270
|
+
deliveryAddress4: rg.deliveryAddress4,
|
|
271
|
+
deliveryAddress5: rg.deliveryAddress5,
|
|
272
|
+
sortedByUser: rg.sortedByUser,
|
|
273
|
+
itemCounts: {
|
|
274
|
+
total,
|
|
275
|
+
pending: total - completed
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}) as ReleaseGoodEntity[]
|
|
279
|
+
}
|
|
280
|
+
|
|
18
281
|
export const findSortingReleaseOrdersByTaskNoResolver = {
|
|
19
282
|
async findSortingReleaseOrdersByTaskNo(_: any, { taskNo }, context: any) {
|
|
283
|
+
const startTime = Date.now()
|
|
20
284
|
const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state
|
|
21
285
|
|
|
22
|
-
let selectedReleaseGood = null
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING, ORDER_STATUS.LOADING, ORDER_STATUS.DONE]
|
|
33
|
-
})
|
|
34
|
-
.andWhere('ws.type = :type', { type: WORKSHEET_TYPE.SORTING })
|
|
35
|
-
.getRawOne()
|
|
36
|
-
|
|
37
|
-
if (ws) {
|
|
38
|
-
if (ws.release_good_status == 'LOADING' || ws.release_good_status == 'DONE') {
|
|
39
|
-
throw new Error(`Release Good already sorted`);
|
|
40
|
-
}
|
|
286
|
+
let selectedReleaseGood: string | null = null
|
|
287
|
+
let task: WorksheetEntity | null = null
|
|
288
|
+
let resolvedTaskNo = taskNo
|
|
289
|
+
|
|
290
|
+
// Step 1: Try to find by release good name first (most common case)
|
|
291
|
+
const releaseGoodResult = await findWorksheetByReleaseGoodName(tx, domain, taskNo)
|
|
292
|
+
|
|
293
|
+
if (releaseGoodResult) {
|
|
294
|
+
selectedReleaseGood = releaseGoodResult.selectedReleaseGood
|
|
295
|
+
resolvedTaskNo = releaseGoodResult.taskNo
|
|
41
296
|
|
|
42
|
-
|
|
43
|
-
|
|
297
|
+
// Only load worksheetDetails if we need them for activation
|
|
298
|
+
const needsActivation = releaseGoodResult.status === 'DEACTIVATED'
|
|
299
|
+
task = await findWorksheetByTaskNo(tx, domain, resolvedTaskNo, needsActivation)
|
|
44
300
|
}
|
|
45
301
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
302
|
+
// Step 2: Try to find by worksheet taskNo directly
|
|
303
|
+
if (!task) {
|
|
304
|
+
task = await findWorksheetByTaskNo(tx, domain, taskNo, false)
|
|
305
|
+
}
|
|
50
306
|
|
|
51
|
-
//
|
|
307
|
+
// Step 3: Try to find by bin location
|
|
52
308
|
if (!task) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
.createQueryBuilder('orderInventory')
|
|
61
|
-
|
|
62
|
-
qb.innerJoinAndSelect('orderInventory.releaseGood', 'releaseGood')
|
|
63
|
-
.innerJoinAndSelect('worksheets', 'ws', `orderInventory.ref_worksheet_id = ws.id AND ws.type = 'BATCH_PICKING'`)
|
|
64
|
-
.innerJoinAndSelect('worksheets', 'ws2', `ws2.task_no = ws.task_no AND ws2.type = 'SORTING'`)
|
|
65
|
-
.innerJoinAndSelect('releaseGood.bizplace', 'bizplace')
|
|
66
|
-
.innerJoinAndSelect('bizplace.domain', 'domain')
|
|
67
|
-
.where('orderInventory.domain_id = :domainId', { domainId: domain.id })
|
|
68
|
-
.andWhere('orderInventory.status IN (:...orderInventoryStatus)', {
|
|
69
|
-
orderInventoryStatus: [ORDER_INVENTORY_STATUS.READY_TO_SORT, ORDER_INVENTORY_STATUS.SORTING]
|
|
70
|
-
})
|
|
71
|
-
.andWhere('orderInventory.bin_location_id = :locationId', { locationId: binLocation.id })
|
|
72
|
-
.andWhere('releaseGood.status IN (:...statuses)', {
|
|
73
|
-
statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING]
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const orderInventoryByBin = await qb.getRawOne()
|
|
77
|
-
if (orderInventoryByBin?.releaseGood_id) {
|
|
78
|
-
taskNo = orderInventoryByBin.ws_task_no
|
|
79
|
-
task = await tx.getRepository(WorksheetEntity).findOne({
|
|
80
|
-
where: {
|
|
81
|
-
taskNo,
|
|
82
|
-
status: In([WORKSHEET_STATUS.DEACTIVATED, WORKSHEET_STATUS.EXECUTING]),
|
|
83
|
-
type: WORKSHEET_TYPE.SORTING,
|
|
84
|
-
domain
|
|
85
|
-
},
|
|
86
|
-
relations: ['worksheetDetails']
|
|
87
|
-
})
|
|
88
|
-
} else {
|
|
89
|
-
throw new Error(`Bin do not have any batch picking order.`)
|
|
90
|
-
}
|
|
309
|
+
const binResult = await findWorksheetByBinLocation(tx, domain, taskNo)
|
|
310
|
+
|
|
311
|
+
if (binResult) {
|
|
312
|
+
resolvedTaskNo = binResult.taskNo
|
|
313
|
+
// Only load worksheetDetails if we need them for activation
|
|
314
|
+
const needsActivation = binResult.status === 'DEACTIVATED'
|
|
315
|
+
task = await findWorksheetByTaskNo(tx, domain, resolvedTaskNo, needsActivation)
|
|
91
316
|
}
|
|
92
317
|
}
|
|
93
318
|
|
|
94
|
-
if (!task)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
domain: domain,
|
|
109
|
-
partnerDomain: task.bizplace?.domain
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
if (directActivateSortingWorksheet != undefined && directActivateSortingWorksheet.value.toLowerCase() == 'true') {
|
|
114
|
-
if (partnerDirectActivateSortingWorksheetSetting != undefined) {
|
|
115
|
-
if (partnerDirectActivateSortingWorksheetSetting.value.toLowerCase() == 'true')
|
|
116
|
-
await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
|
|
117
|
-
} else {
|
|
118
|
-
await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
|
|
122
|
-
}
|
|
319
|
+
if (!task) {
|
|
320
|
+
throw new Error('Unable to find task no.')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Step 4: Handle activation if needed
|
|
324
|
+
await handleWorksheetActivation(tx, domain, user, task)
|
|
325
|
+
|
|
326
|
+
// Step 5: Get release goods with counts
|
|
327
|
+
const releaseGoods = await getReleaseGoodsWithCounts(tx, domain, resolvedTaskNo)
|
|
328
|
+
|
|
329
|
+
// Performance logging (only in development/debug mode)
|
|
330
|
+
const elapsed = Date.now() - startTime
|
|
331
|
+
if (elapsed > 1000) {
|
|
332
|
+
console.warn(`[PERF] findSortingReleaseOrdersByTaskNo took ${elapsed}ms for taskNo: ${taskNo}`)
|
|
123
333
|
}
|
|
124
334
|
|
|
125
|
-
|
|
126
|
-
.getRepository(WorksheetEntity)
|
|
127
|
-
.createQueryBuilder('ws')
|
|
128
|
-
.select('rg.id as id')
|
|
129
|
-
.addSelect('rg.name as name')
|
|
130
|
-
.addSelect('rg.refNo as "refNo"')
|
|
131
|
-
.addSelect('rg.refNo2 as "refNo2"')
|
|
132
|
-
.addSelect('rg.status as status')
|
|
133
|
-
.addSelect('rg.district as "district"')
|
|
134
|
-
.addSelect('rg.attention_company as "attentionCompany"')
|
|
135
|
-
.addSelect('rg.delivery_address_1 as "deliveryAddress1"')
|
|
136
|
-
.addSelect('rg.delivery_address_2 as "deliveryAddress2"')
|
|
137
|
-
.addSelect('rg.delivery_address_3 as "deliveryAddress3"')
|
|
138
|
-
.addSelect('rg.delivery_address_4 as "deliveryAddress4"')
|
|
139
|
-
.addSelect('rg.delivery_address_5 as "deliveryAddress5"')
|
|
140
|
-
.addSelect('user.name as "sortedByUser"')
|
|
141
|
-
.innerJoin(Domain, 'domain', 'ws.domain_id = domain.id')
|
|
142
|
-
.innerJoin(WorksheetDetailEntity, 'wsd', 'ws.id = wsd.worksheet_id')
|
|
143
|
-
.innerJoin(OrderInventoryEntity, 'oi', 'oi.id = wsd.target_inventory_id')
|
|
144
|
-
.innerJoin(ReleaseGoodEntity, 'rg', 'rg.id = oi.release_good_id')
|
|
145
|
-
.leftJoin(User, 'user', 'rg.sorted_by_id = user.id')
|
|
146
|
-
.where('domain.id = :domainId', { domainId: domain.id })
|
|
147
|
-
.andWhere('ws.taskNo = :taskNo', { taskNo: taskNo })
|
|
148
|
-
.andWhere('ws.type = :worksheetType', { worksheetType: WORKSHEET_TYPE.SORTING })
|
|
149
|
-
.andWhere('ws.status = :worksheetStatus', { worksheetStatus: WORKSHEET_STATUS.EXECUTING })
|
|
150
|
-
.groupBy('rg.id')
|
|
151
|
-
.addGroupBy('rg.name')
|
|
152
|
-
.addGroupBy('rg.status')
|
|
153
|
-
.addGroupBy('user.name')
|
|
154
|
-
.orderBy('rg.createdAt', 'ASC')
|
|
155
|
-
|
|
156
|
-
const releaseGoods: ReleaseGoodEntity[] = await qb.getRawMany()
|
|
157
|
-
|
|
158
|
-
return { releaseGoods, taskNo, selectedReleaseGood }
|
|
335
|
+
return { releaseGoods, taskNo: resolvedTaskNo, selectedReleaseGood }
|
|
159
336
|
}
|
|
160
337
|
}
|
|
@@ -24,7 +24,7 @@ import { havingVasResolver } from './having-vas'
|
|
|
24
24
|
import { Mutations as InspectMutations } from './inspecting'
|
|
25
25
|
import { inventoriesByPalletResolver } from './inventories-by-pallet'
|
|
26
26
|
import { loadedInventories } from './loaded-inventories'
|
|
27
|
-
import { Mutations as LoadingMutations } from './loading'
|
|
27
|
+
import { Mutations as LoadingMutations, Query as LoadingQuery } from './loading'
|
|
28
28
|
import { loadingWorksheetResolver } from './loading-worksheet'
|
|
29
29
|
import { notTallyTargetInventoriesResolver } from './not-tally-target-inventories'
|
|
30
30
|
import { Mutations as PackingMutations } from './packing'
|
|
@@ -115,7 +115,8 @@ export const Query = {
|
|
|
115
115
|
...findSortingReleaseOrdersByTaskNoResolver,
|
|
116
116
|
...findReleaseOrdersByTaskNoResolver,
|
|
117
117
|
...fetchDeliveryOrderROResolver,
|
|
118
|
-
...putawayReplenishmentWorksheetResolver
|
|
118
|
+
...putawayReplenishmentWorksheetResolver,
|
|
119
|
+
...LoadingQuery
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
export const Mutation = {
|
|
@@ -2,6 +2,7 @@ import { activateLoadingResolver } from './activate-loading'
|
|
|
2
2
|
import { loadingResolver } from './loading'
|
|
3
3
|
import { undoLoadingResolver } from './undo-loading'
|
|
4
4
|
import { completeLoadingResolver } from './complete-loading'
|
|
5
|
+
import { validateQcSealsResolver } from './validate-qc-seals'
|
|
5
6
|
|
|
6
7
|
export const Mutations = {
|
|
7
8
|
...activateLoadingResolver,
|
|
@@ -9,3 +10,7 @@ export const Mutations = {
|
|
|
9
10
|
...undoLoadingResolver,
|
|
10
11
|
...completeLoadingResolver
|
|
11
12
|
}
|
|
13
|
+
|
|
14
|
+
export const Query = {
|
|
15
|
+
...validateQcSealsResolver
|
|
16
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { EntityManager, IsNull } from 'typeorm'
|
|
2
|
+
import { Domain } from '@things-factory/shell'
|
|
3
|
+
import { User } from '@things-factory/auth-base'
|
|
4
|
+
import { OrderTote, OrderToteSeal, ReleaseGood } from '@things-factory/sales-base'
|
|
5
|
+
import { Setting } from '@things-factory/setting-base'
|
|
6
|
+
|
|
7
|
+
export const validateQcSealsResolver = {
|
|
8
|
+
async validateQcSeals(
|
|
9
|
+
_: any,
|
|
10
|
+
{ releaseGoodNo },
|
|
11
|
+
context: any
|
|
12
|
+
): Promise<{ valid: boolean; error?: string; minimumSealNumber?: number }> {
|
|
13
|
+
const { tx, domain, user }: { tx: EntityManager; domain: Domain; user: User } = context.state
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const releaseGood: ReleaseGood = await tx.getRepository(ReleaseGood).findOne({
|
|
17
|
+
where: { domain, name: releaseGoodNo },
|
|
18
|
+
relations: ['bizplace', 'bizplace.domain']
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if (!releaseGood) {
|
|
22
|
+
return { valid: false, error: 'Release good not found', minimumSealNumber: 0 }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check enable-tote-scanning setting
|
|
26
|
+
let enableToteScanningSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
27
|
+
where: {
|
|
28
|
+
domain: domain,
|
|
29
|
+
category: 'id-rule',
|
|
30
|
+
name: 'enable-tote-scanning'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Check minimum-seal-number setting (needed for banner display)
|
|
35
|
+
let minimumSealNumberSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
36
|
+
where: {
|
|
37
|
+
domain: domain,
|
|
38
|
+
category: 'id-rule',
|
|
39
|
+
name: 'minimum-seal-number'
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const minimumSealNumber = parseInt(minimumSealNumberSetting?.value || '0', 10)
|
|
44
|
+
|
|
45
|
+
if (enableToteScanningSetting) {
|
|
46
|
+
// enable-tote-scanning is numeric: 0 = disabled, >= 1 = enabled
|
|
47
|
+
const enableToteScanningValue = parseInt(enableToteScanningSetting.value || '0', 10)
|
|
48
|
+
|
|
49
|
+
if (enableToteScanningValue >= 1) {
|
|
50
|
+
// Check if there are unsealed totes (closedDate is null)
|
|
51
|
+
const foundNotSealedOrderTote = await tx
|
|
52
|
+
.getRepository(OrderTote)
|
|
53
|
+
.findOne({ where: { releaseGood, closedDate: IsNull() } })
|
|
54
|
+
|
|
55
|
+
if (foundNotSealedOrderTote) {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: `Please seal the tote(s) before proceeding. Minimum ${minimumSealNumber} seal(s) required.`,
|
|
59
|
+
minimumSealNumber
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If minimum-seal-number > 0, validate seal counts per tote
|
|
64
|
+
if (minimumSealNumber > 0) {
|
|
65
|
+
// Get all order totes for this release good
|
|
66
|
+
const orderTotes: OrderTote[] = await tx.getRepository(OrderTote).find({
|
|
67
|
+
where: { releaseGood },
|
|
68
|
+
relations: ['orderToteSeals']
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Validate each tote has at least minimum-seal-number seals
|
|
72
|
+
for (const orderTote of orderTotes) {
|
|
73
|
+
const sealCount = orderTote.orderToteSeals?.length || 0
|
|
74
|
+
if (sealCount < minimumSealNumber) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: `Tote ${orderTote.name} has ${sealCount} seal(s), but minimum ${minimumSealNumber} seal(s) required`,
|
|
78
|
+
minimumSealNumber
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { valid: true, minimumSealNumber }
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return { valid: false, error: error.message || 'Validation failed', minimumSealNumber: 0 }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -22,6 +22,7 @@ import { ProductApproval } from './product-approval'
|
|
|
22
22
|
import { ReleaseGoodWorksheet } from './release-good-worksheet'
|
|
23
23
|
import { ReturnOrderWorksheet } from './return-order-worksheet'
|
|
24
24
|
import { SellercraftDocument } from './sellercraft-document'
|
|
25
|
+
import { ValidateQcSealsResult } from './validate-qc-seals-result'
|
|
25
26
|
import { VasOrderWorksheet } from './vas-order-worksheet'
|
|
26
27
|
import { WebspertDocument } from './webspert-document'
|
|
27
28
|
import { Worksheet } from './worksheet'
|
|
@@ -864,6 +865,8 @@ export const Query = /* GraphQL */ `
|
|
|
864
865
|
findSortingReleaseOrdersByTaskNo(taskNo: String!): FindReleaseOrdersByTaskNo @privilege(category: "worksheet", privilege: "query") @transaction
|
|
865
866
|
|
|
866
867
|
findReleaseOrdersByTaskNo(taskNo: String!): FindReleaseOrdersByTaskNo @privilege(category: "worksheet", privilege: "query") @transaction
|
|
868
|
+
|
|
869
|
+
validateQcSeals(releaseGoodNo: String!): ValidateQcSealsResult @privilege(category: "worksheet", privilege: "query") @transaction
|
|
867
870
|
`
|
|
868
871
|
|
|
869
872
|
export const Types = /* GraphQL */ [
|
|
@@ -898,5 +901,6 @@ export const Types = /* GraphQL */ [
|
|
|
898
901
|
DeliveryOrderRO,
|
|
899
902
|
GenerateBatchPickInfo,
|
|
900
903
|
MultipleReleaseGoodWorksheet,
|
|
901
|
-
LocationFilter
|
|
904
|
+
LocationFilter,
|
|
905
|
+
ValidateQcSealsResult
|
|
902
906
|
]
|