@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
|
|
2
|
+
|
|
3
|
+
import { Domain } from '@things-factory/shell'
|
|
4
|
+
import { User } from '@things-factory/auth-base'
|
|
5
|
+
|
|
6
|
+
@Entity('route_label_sequence_logs')
|
|
7
|
+
@Index('ix_route_label_sequence_logs_0', (l: RouteLabelSequenceLog) => [
|
|
8
|
+
l.domain,
|
|
9
|
+
l.releaseGoodId,
|
|
10
|
+
l.field,
|
|
11
|
+
l.createdAt
|
|
12
|
+
])
|
|
13
|
+
@Index('ix_route_label_sequence_logs_1', (l: RouteLabelSequenceLog) => [l.domain, l.field, l.createdAt])
|
|
14
|
+
export class RouteLabelSequenceLog {
|
|
15
|
+
@PrimaryGeneratedColumn('uuid')
|
|
16
|
+
readonly id: string
|
|
17
|
+
|
|
18
|
+
@ManyToOne(type => Domain)
|
|
19
|
+
domain?: Domain
|
|
20
|
+
|
|
21
|
+
@RelationId((l: RouteLabelSequenceLog) => l.domain)
|
|
22
|
+
domainId?: string
|
|
23
|
+
|
|
24
|
+
@Column()
|
|
25
|
+
releaseGoodId: string
|
|
26
|
+
|
|
27
|
+
@Column()
|
|
28
|
+
field: string
|
|
29
|
+
|
|
30
|
+
@Column({ type: 'int' })
|
|
31
|
+
from: number
|
|
32
|
+
|
|
33
|
+
@Column({ type: 'int' })
|
|
34
|
+
to: number
|
|
35
|
+
|
|
36
|
+
@Column({ type: 'int' })
|
|
37
|
+
qty: number
|
|
38
|
+
|
|
39
|
+
@CreateDateColumn()
|
|
40
|
+
createdAt?: Date
|
|
41
|
+
|
|
42
|
+
@ManyToOne(type => User, { nullable: true })
|
|
43
|
+
creator?: User
|
|
44
|
+
|
|
45
|
+
@RelationId((l: RouteLabelSequenceLog) => l.creator)
|
|
46
|
+
creatorId?: string
|
|
47
|
+
}
|
|
@@ -8,6 +8,7 @@ import * as Reports from './reports'
|
|
|
8
8
|
import * as InventoryComparison from './inventory-comparison'
|
|
9
9
|
import * as ShippingProviders from './shipping-provider'
|
|
10
10
|
import * as WarehouseInventoryAdjustment from './warehouse-inventory-adjustment'
|
|
11
|
+
import * as RouteLabel from './route-label'
|
|
11
12
|
import * as ZoneWorksheet from './zone-worksheet'
|
|
12
13
|
import * as Outbound from './outbound'
|
|
13
14
|
|
|
@@ -21,6 +22,7 @@ export const queries = [
|
|
|
21
22
|
Other.Query,
|
|
22
23
|
Reports.Query,
|
|
23
24
|
ShippingProviders.Query,
|
|
25
|
+
RouteLabel.Query,
|
|
24
26
|
ZoneWorksheet.Query,
|
|
25
27
|
Outbound.Query
|
|
26
28
|
]
|
|
@@ -28,6 +30,7 @@ export const queries = [
|
|
|
28
30
|
export const mutations = [
|
|
29
31
|
Other.Mutation,
|
|
30
32
|
WarehouseInventoryAdjustment.Mutation,
|
|
33
|
+
RouteLabel.Mutation,
|
|
31
34
|
ZoneWorksheet.Mutation,
|
|
32
35
|
Outbound.Mutation
|
|
33
36
|
]
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ReleaseGood,
|
|
7
7
|
OrderInventory,
|
|
8
8
|
OrderProduct,
|
|
9
|
+
OrderTote,
|
|
9
10
|
PowrupController,
|
|
10
11
|
OrderPackage,
|
|
11
12
|
ORDER_STATUS,
|
|
@@ -17,6 +18,11 @@ import { Powrup } from '@things-factory/integration-powrup'
|
|
|
17
18
|
import { Setting } from '@things-factory/setting-base'
|
|
18
19
|
import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
|
|
19
20
|
|
|
21
|
+
function parseNonNegativeInt(value: any): number {
|
|
22
|
+
const n = parseInt(value ?? '0', 10)
|
|
23
|
+
return Number.isFinite(n) ? Math.max(0, n) : 0
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export const completeLoad = {
|
|
21
27
|
async completeLoad(_: any, { orderId, worksheetId }, context: any) {
|
|
22
28
|
const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state
|
|
@@ -119,6 +125,41 @@ async function completeLoading(
|
|
|
119
125
|
releaseGood.ownTransport = ownTransportFlag
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
// If minimum seal is 0 (or invalid/negative), auto-close any open totes for this order.
|
|
129
|
+
// This supports the "0 seal required" scenario without blocking tote usage during QC/loading.
|
|
130
|
+
try {
|
|
131
|
+
let sealNoSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
132
|
+
where: {
|
|
133
|
+
domain: (releaseGood as any).bizplace?.domain,
|
|
134
|
+
name: 'minimum-seal-number'
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (!sealNoSetting) {
|
|
139
|
+
sealNoSetting = await tx.getRepository(Setting).findOne({
|
|
140
|
+
where: {
|
|
141
|
+
domain,
|
|
142
|
+
name: 'minimum-seal-number'
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const minimumSealRequired = parseNonNegativeInt(sealNoSetting?.value)
|
|
148
|
+
if (minimumSealRequired <= 0) {
|
|
149
|
+
await tx
|
|
150
|
+
.getRepository(OrderTote)
|
|
151
|
+
.createQueryBuilder()
|
|
152
|
+
.update(OrderTote)
|
|
153
|
+
.set({ closedDate: currentTime, updater: user })
|
|
154
|
+
.where('release_good_id = :releaseGoodId', { releaseGoodId: releaseGood.id })
|
|
155
|
+
.andWhere('closed_date IS NULL')
|
|
156
|
+
.execute()
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// do not fail completion if tote auto-close fails
|
|
160
|
+
console.error('Failed to auto-close order totes:', e)
|
|
161
|
+
}
|
|
162
|
+
|
|
122
163
|
await updateOrderInventories(tx, orderInventories, ORDER_INVENTORY_STATUS.TERMINATED, user, currentTime)
|
|
123
164
|
await updateWorksheet(tx, worksheet, WORKSHEET_STATUS.DONE, user, currentTime)
|
|
124
165
|
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.LoadingCompleted)
|
|
@@ -210,4 +251,4 @@ async function updateReleaseGood(tx, releaseGood, status, user, currentTime) {
|
|
|
210
251
|
if (status === ORDER_STATUS.DONE) {
|
|
211
252
|
webhookHandler(releaseGood, releaseGood.bizplace, WebhookEventsEnum.ReleaseOrderCompleted)
|
|
212
253
|
}
|
|
213
|
-
}
|
|
254
|
+
}
|
|
@@ -31,10 +31,17 @@ const buildfindReleaseGoodByNameQuery = (tx: EntityManager, domainId: string, or
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Builds a query to find order inventories
|
|
34
|
+
* Builds a query to find order inventories
|
|
35
|
+
* @param tx - Entity manager
|
|
36
|
+
* @param domainId - Domain ID
|
|
37
|
+
* @param options - Optional filters: locationId for bin location, releaseGoodId for release good
|
|
35
38
|
*/
|
|
36
|
-
const buildOrderInventoryQuery = (
|
|
37
|
-
|
|
39
|
+
const buildOrderInventoryQuery = (
|
|
40
|
+
tx: EntityManager,
|
|
41
|
+
domainId: string,
|
|
42
|
+
options?: { locationId?: string; releaseGoodId?: string }
|
|
43
|
+
) => {
|
|
44
|
+
const queryBuilder = tx
|
|
38
45
|
.getRepository(OrderInventory)
|
|
39
46
|
.createQueryBuilder('orderInventory')
|
|
40
47
|
.leftJoinAndSelect('orderInventory.inventory', 'inventory')
|
|
@@ -50,19 +57,32 @@ const buildOrderInventoryQuery = (tx: EntityManager, domainId: string, locationI
|
|
|
50
57
|
ORDER_INVENTORY_STATUS.LOADED
|
|
51
58
|
]
|
|
52
59
|
})
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
|
|
61
|
+
if (options?.locationId) {
|
|
62
|
+
queryBuilder.andWhere('orderInventory.bin_location_id = :locationId', { locationId: options.locationId })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options?.releaseGoodId) {
|
|
66
|
+
queryBuilder.andWhere('orderInventory.release_good_id = :releaseGoodId', { releaseGoodId: options.releaseGoodId })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return queryBuilder.orderBy('orderInventory.createdAt', 'ASC')
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
73
|
* Transforms order inventory data into the required worksheet format
|
|
74
|
+
* @param orderInventories - Array of order inventories
|
|
75
|
+
* @param releaseGood - Optional release good (when scanning by order number)
|
|
59
76
|
*/
|
|
60
|
-
const transformToWorksheetFormat = (orderInventories: OrderInventory[]) => {
|
|
77
|
+
const transformToWorksheetFormat = (orderInventories: OrderInventory[], releaseGood?: ReleaseGood) => {
|
|
61
78
|
if (!orderInventories.length) return null
|
|
62
79
|
|
|
63
80
|
// Extract unique bin location names
|
|
64
81
|
const uniqueBinLocations = [...new Set(orderInventories.map(oi => oi.binLocation?.name).filter(Boolean))]
|
|
65
82
|
|
|
83
|
+
// Get release good from first order inventory if not provided
|
|
84
|
+
const firstReleaseGood = releaseGood || orderInventories[0]?.releaseGood
|
|
85
|
+
|
|
66
86
|
return {
|
|
67
87
|
worksheetInfo: {
|
|
68
88
|
worksheet: {
|
|
@@ -73,8 +93,22 @@ const transformToWorksheetFormat = (orderInventories: OrderInventory[]) => {
|
|
|
73
93
|
},
|
|
74
94
|
bizplaceName: null,
|
|
75
95
|
startedAt: null,
|
|
76
|
-
releaseGood:
|
|
77
|
-
|
|
96
|
+
releaseGood: firstReleaseGood
|
|
97
|
+
? {
|
|
98
|
+
id: firstReleaseGood.id,
|
|
99
|
+
name: firstReleaseGood.name,
|
|
100
|
+
attentionCompany: firstReleaseGood.attentionCompany,
|
|
101
|
+
attentionTo: firstReleaseGood.attentionTo,
|
|
102
|
+
deliveryAddress1: firstReleaseGood.deliveryAddress1,
|
|
103
|
+
deliveryAddress2: firstReleaseGood.deliveryAddress2,
|
|
104
|
+
status: firstReleaseGood.status,
|
|
105
|
+
refNo: firstReleaseGood.refNo,
|
|
106
|
+
refNo2: firstReleaseGood.refNo2,
|
|
107
|
+
refNo3: firstReleaseGood.refNo3
|
|
108
|
+
}
|
|
109
|
+
: null,
|
|
110
|
+
binLocationName: uniqueBinLocations.join(', '), // Join multiple bin locations with comma
|
|
111
|
+
isReleaseGoodScan: !!releaseGood // Flag to indicate if this result came from scanning by release good
|
|
78
112
|
},
|
|
79
113
|
worksheetDetailInfos: orderInventories.map(oi => ({
|
|
80
114
|
id: null,
|
|
@@ -96,6 +130,7 @@ const transformToWorksheetFormat = (orderInventories: OrderInventory[]) => {
|
|
|
96
130
|
id: oi.id,
|
|
97
131
|
batchId: oi.batchId,
|
|
98
132
|
packingType: oi.packingType,
|
|
133
|
+
refWorksheetId: oi.refWorksheetId,
|
|
99
134
|
product: oi.inventory?.product
|
|
100
135
|
? {
|
|
101
136
|
id: oi.inventory.product.id,
|
|
@@ -132,17 +167,25 @@ export const findLoadableReleaseGoodByBin = {
|
|
|
132
167
|
if (foundReleaseGood && foundReleaseGood.status == ORDER_STATUS.DONE) {
|
|
133
168
|
throw new OutboundOrderError('This order is DONE', 'ORDER_DONE')
|
|
134
169
|
} else if (foundReleaseGood) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
170
|
+
// Found release good by order number - query its order inventories and return worksheet format
|
|
171
|
+
orderInventoryByBin = await buildOrderInventoryQuery(tx, domain.id, {
|
|
172
|
+
releaseGoodId: foundReleaseGood.id
|
|
173
|
+
}).getMany()
|
|
174
|
+
|
|
175
|
+
if (orderInventoryByBin.length > 0) {
|
|
176
|
+
// Pass the release good to indicate this is a release good scan
|
|
177
|
+
return transformToWorksheetFormat(orderInventoryByBin, foundReleaseGood)
|
|
178
|
+
} else {
|
|
179
|
+
throw new OutboundOrderError('There is no item in this order', 'NO_ITEMS_IN_ORDER')
|
|
180
|
+
}
|
|
139
181
|
} else {
|
|
182
|
+
// Try to find by bin location
|
|
140
183
|
const binLocation: Location = await tx.getRepository(Location).findOne({
|
|
141
184
|
where: { domain, name: orderNo }
|
|
142
185
|
})
|
|
143
186
|
|
|
144
187
|
if (binLocation) {
|
|
145
|
-
orderInventoryByBin = await buildOrderInventoryQuery(tx, domain.id, binLocation.id).getMany()
|
|
188
|
+
orderInventoryByBin = await buildOrderInventoryQuery(tx, domain.id, { locationId: binLocation.id }).getMany()
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
if (orderInventoryByBin.length > 0) {
|
|
@@ -4,49 +4,126 @@ import { Location } from '@things-factory/warehouse-base'
|
|
|
4
4
|
import { Domain } from '@things-factory/shell'
|
|
5
5
|
import { OrderInventory, ORDER_INVENTORY_STATUS, ORDER_STATUS, ReleaseGood } from '@things-factory/sales-base'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Helper function to get all inventory counts in a single query
|
|
9
|
+
* Returns counts for LOADING status, LOADED status, and total
|
|
10
|
+
*/
|
|
11
|
+
async function getInventoryCounts(
|
|
12
|
+
tx: EntityManager,
|
|
13
|
+
domainId: string,
|
|
14
|
+
releaseGoodId: string,
|
|
15
|
+
binLocationId?: string
|
|
16
|
+
): Promise<{
|
|
17
|
+
loadingTotal: number
|
|
18
|
+
loadingCompleted: number
|
|
19
|
+
loadedTotal: number
|
|
20
|
+
loadedCompleted: number
|
|
21
|
+
total: number
|
|
22
|
+
}> {
|
|
23
|
+
const qb = tx
|
|
24
|
+
.getRepository(OrderInventory)
|
|
25
|
+
.createQueryBuilder('orderInventory')
|
|
26
|
+
.select('COUNT(*)', 'total')
|
|
27
|
+
.addSelect(
|
|
28
|
+
`SUM(CASE WHEN orderInventory.status = :loadingStatus THEN 1 ELSE 0 END)`,
|
|
29
|
+
'loadingTotal'
|
|
30
|
+
)
|
|
31
|
+
.addSelect(
|
|
32
|
+
`SUM(CASE WHEN orderInventory.status = :loadedStatus THEN 1 ELSE 0 END)`,
|
|
33
|
+
'loadedTotal'
|
|
34
|
+
)
|
|
35
|
+
.where('orderInventory.release_good_id = :releaseGoodId', { releaseGoodId })
|
|
36
|
+
.andWhere('orderInventory.domain_id = :domainId', { domainId })
|
|
37
|
+
.setParameter('loadingStatus', ORDER_INVENTORY_STATUS.LOADING)
|
|
38
|
+
.setParameter('loadedStatus', ORDER_INVENTORY_STATUS.LOADED)
|
|
39
|
+
|
|
40
|
+
if (binLocationId) {
|
|
41
|
+
qb.andWhere('orderInventory.bin_location_id = :binLocationId', { binLocationId })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await qb.getRawOne()
|
|
45
|
+
|
|
46
|
+
const loadingTotal = parseInt(result.loadingTotal) || 0
|
|
47
|
+
const loadedTotal = parseInt(result.loadedTotal) || 0
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
loadingTotal,
|
|
51
|
+
loadingCompleted: loadedTotal, // Items that moved from LOADING to LOADED
|
|
52
|
+
loadedTotal,
|
|
53
|
+
loadedCompleted: loadedTotal, // LOADED items are already complete
|
|
54
|
+
total: parseInt(result.total) || 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Assigns itemCounts and tabCounts to a release good based on inventory counts
|
|
60
|
+
*/
|
|
61
|
+
function assignCountsToReleaseGood(
|
|
62
|
+
releaseGood: ReleaseGood,
|
|
63
|
+
counts: Awaited<ReturnType<typeof getInventoryCounts>>,
|
|
64
|
+
tabName: string,
|
|
65
|
+
isBinScan: boolean = false
|
|
66
|
+
): void {
|
|
67
|
+
const { loadingTotal, loadingCompleted, loadedTotal, loadedCompleted, total } = counts
|
|
68
|
+
|
|
69
|
+
// Calculate completed count for current tab (backward compatibility)
|
|
70
|
+
let currentCompleted: number
|
|
71
|
+
if (isBinScan) {
|
|
72
|
+
currentCompleted = tabName === 'LOADING' ? loadingTotal : loadedTotal
|
|
73
|
+
} else {
|
|
74
|
+
// ORDER scan: QC tab considers LOADING+LOADED as completed
|
|
75
|
+
currentCompleted = tabName === 'QC' ? loadingTotal + loadedTotal : loadedTotal
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Backward compatibility: itemCounts for current tab
|
|
79
|
+
releaseGood['itemCounts'] = {
|
|
80
|
+
total,
|
|
81
|
+
pending: total - currentCompleted
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// tabCounts for both LOADING and LOADED tabs
|
|
85
|
+
releaseGood['tabCounts'] = {
|
|
86
|
+
LOADING: {
|
|
87
|
+
total: loadingTotal,
|
|
88
|
+
completed: loadingCompleted,
|
|
89
|
+
pending: loadingTotal - loadingCompleted
|
|
90
|
+
},
|
|
91
|
+
LOADED: {
|
|
92
|
+
total: loadedTotal,
|
|
93
|
+
completed: loadedCompleted,
|
|
94
|
+
pending: loadedTotal - loadedCompleted
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
7
99
|
export const findLoadableReleaseGood = {
|
|
8
100
|
async findLoadableReleaseGood(_: any, { orderNo, tabName }, context: any): Promise<ReleaseGood[]> {
|
|
9
101
|
const { domain, tx }: { domain: Domain; tx: EntityManager } = context.state
|
|
10
102
|
|
|
11
103
|
let releaseGoods = []
|
|
12
104
|
|
|
13
|
-
//put query in util same as find-loadable-release-good-by-bin.ts
|
|
14
|
-
//jiahhao
|
|
15
105
|
let foundReleaseGood: ReleaseGood = await tx
|
|
16
106
|
.getRepository(ReleaseGood)
|
|
17
107
|
.createQueryBuilder('releaseGood')
|
|
18
108
|
.where('releaseGood.domain_id = :domainId', { domainId: domain.id })
|
|
19
109
|
.andWhere('releaseGood.name = :orderNo', { orderNo })
|
|
20
110
|
.andWhere('releaseGood.status IN (:...status)', {
|
|
21
|
-
status: [
|
|
111
|
+
status: [
|
|
112
|
+
ORDER_STATUS.SORTING,
|
|
113
|
+
ORDER_STATUS.LOADING,
|
|
114
|
+
ORDER_STATUS.PROCESSING,
|
|
115
|
+
ORDER_STATUS.PARTIAL_PROCESSING,
|
|
116
|
+
ORDER_STATUS.DONE
|
|
117
|
+
]
|
|
22
118
|
})
|
|
23
119
|
.getOne()
|
|
24
120
|
|
|
25
121
|
if (foundReleaseGood && foundReleaseGood.status == ORDER_STATUS.DONE) {
|
|
26
122
|
throw new Error('This order is DONE')
|
|
27
123
|
} else if (foundReleaseGood) {
|
|
28
|
-
// Get counts
|
|
29
|
-
const counts = await tx
|
|
30
|
-
|
|
31
|
-
.createQueryBuilder('orderInventory')
|
|
32
|
-
.select('COUNT(*)', 'total')
|
|
33
|
-
.addSelect(`SUM(CASE WHEN orderInventory.status IN (:...completedStatuses) THEN 1 ELSE 0 END)`, 'completed')
|
|
34
|
-
.where('orderInventory.release_good_id = :releaseGoodId', { releaseGoodId: foundReleaseGood.id })
|
|
35
|
-
.andWhere('orderInventory.domain_id = :domainId', { domainId: domain.id })
|
|
36
|
-
.setParameter(
|
|
37
|
-
'completedStatuses',
|
|
38
|
-
tabName === 'QC'
|
|
39
|
-
? [ORDER_INVENTORY_STATUS.LOADING, ORDER_INVENTORY_STATUS.LOADED]
|
|
40
|
-
: tabName === 'LOADING'
|
|
41
|
-
? [ORDER_INVENTORY_STATUS.LOADED]
|
|
42
|
-
: [ORDER_INVENTORY_STATUS.LOADED]
|
|
43
|
-
)
|
|
44
|
-
.getRawOne()
|
|
45
|
-
|
|
46
|
-
foundReleaseGood['itemCounts'] = {
|
|
47
|
-
total: parseInt(counts.total) || 0,
|
|
48
|
-
pending: parseInt(counts.completed) || 0
|
|
49
|
-
}
|
|
124
|
+
// Get all counts in a single query
|
|
125
|
+
const counts = await getInventoryCounts(tx, domain.id, foundReleaseGood.id)
|
|
126
|
+
assignCountsToReleaseGood(foundReleaseGood, counts, tabName, false)
|
|
50
127
|
|
|
51
128
|
releaseGoods.push(foundReleaseGood)
|
|
52
129
|
return releaseGoods
|
|
@@ -56,7 +133,8 @@ export const findLoadableReleaseGood = {
|
|
|
56
133
|
})
|
|
57
134
|
|
|
58
135
|
if (binLocation) {
|
|
59
|
-
//
|
|
136
|
+
// BIN scan: Find all OrderInventory linked to this BIN regardless of status
|
|
137
|
+
// This allows UI to see orders even when all items are LOADED (so it can auto-switch to LOADED tab)
|
|
60
138
|
const orderInventoryByBin: OrderInventory[] = await tx
|
|
61
139
|
.getRepository(OrderInventory)
|
|
62
140
|
.createQueryBuilder('orderInventory')
|
|
@@ -65,18 +143,19 @@ export const findLoadableReleaseGood = {
|
|
|
65
143
|
.innerJoinAndSelect('bizplace.domain', 'domain')
|
|
66
144
|
.leftJoinAndSelect('releaseGood.sortedBy', 'sortedBy')
|
|
67
145
|
.where('orderInventory.domain_id = :domainId', { domainId: domain.id })
|
|
68
|
-
.andWhere('orderInventory.status = :orderInventoryStatus', {
|
|
69
|
-
orderInventoryStatus:
|
|
70
|
-
tabName === 'QC'
|
|
71
|
-
? ORDER_INVENTORY_STATUS.SORTING
|
|
72
|
-
: tabName === 'LOADING'
|
|
73
|
-
? ORDER_INVENTORY_STATUS.LOADING
|
|
74
|
-
: ORDER_INVENTORY_STATUS.LOADED
|
|
75
|
-
})
|
|
76
146
|
.andWhere('orderInventory.bin_location_id = :locationId', { locationId: binLocation.id })
|
|
77
147
|
.andWhere('releaseGood.status IN (:...status)', {
|
|
78
148
|
status: [ORDER_STATUS.LOADING, ORDER_STATUS.PROCESSING, ORDER_STATUS.PARTIAL_PROCESSING]
|
|
79
149
|
})
|
|
150
|
+
// Include OrderInventory in relevant statuses for loading workflow
|
|
151
|
+
// This ensures we only get items that are part of the loading process
|
|
152
|
+
.andWhere('orderInventory.status IN (:...relevantStatuses)', {
|
|
153
|
+
relevantStatuses: [
|
|
154
|
+
ORDER_INVENTORY_STATUS.SORTING,
|
|
155
|
+
ORDER_INVENTORY_STATUS.LOADING,
|
|
156
|
+
ORDER_INVENTORY_STATUS.LOADED
|
|
157
|
+
]
|
|
158
|
+
})
|
|
80
159
|
.orderBy('releaseGood.createdAt', 'ASC')
|
|
81
160
|
.getMany()
|
|
82
161
|
|
|
@@ -88,39 +167,23 @@ export const findLoadableReleaseGood = {
|
|
|
88
167
|
return acc
|
|
89
168
|
}, [])
|
|
90
169
|
|
|
91
|
-
// Get counts for each release good
|
|
170
|
+
// Get counts for each release good (scoped to this BIN)
|
|
92
171
|
for (const releaseGood of releaseGoods) {
|
|
93
|
-
const counts = await tx
|
|
94
|
-
|
|
95
|
-
.createQueryBuilder('orderInventory')
|
|
96
|
-
.select('COUNT(*)', 'total')
|
|
97
|
-
.addSelect(`SUM(CASE WHEN orderInventory.status IN (:...completedStatuses) THEN 1 ELSE 0 END)`, 'completed')
|
|
98
|
-
.where('orderInventory.release_good_id = :releaseGoodId', { releaseGoodId: releaseGood.id })
|
|
99
|
-
.andWhere('orderInventory.domain_id = :domainId', { domainId: domain.id })
|
|
100
|
-
.setParameter(
|
|
101
|
-
'completedStatuses',
|
|
102
|
-
tabName === 'LOADING'
|
|
103
|
-
? [ORDER_INVENTORY_STATUS.LOADING]
|
|
104
|
-
: tabName === 'LOADED'
|
|
105
|
-
? [ORDER_INVENTORY_STATUS.LOADED]
|
|
106
|
-
: [ORDER_INVENTORY_STATUS.LOADED]
|
|
107
|
-
)
|
|
108
|
-
.getRawOne()
|
|
109
|
-
|
|
110
|
-
releaseGood['itemCounts'] = {
|
|
111
|
-
total: parseInt(counts.total) || 0,
|
|
112
|
-
pending: parseInt(counts.completed) || 0
|
|
113
|
-
}
|
|
172
|
+
const counts = await getInventoryCounts(tx, domain.id, releaseGood.id, binLocation.id)
|
|
173
|
+
assignCountsToReleaseGood(releaseGood, counts, tabName, true)
|
|
114
174
|
}
|
|
115
175
|
}
|
|
116
176
|
|
|
177
|
+
// Return releaseGoods if found, even if LOADING.total=0 (UI will auto-switch to LOADED tab)
|
|
178
|
+
// Only throw error if BIN has no OrderInventory items at all
|
|
117
179
|
if (releaseGoods.length > 0) return releaseGoods
|
|
118
180
|
else {
|
|
181
|
+
// No releaseGoods found in this BIN - BIN is empty or has no items in loading workflow
|
|
119
182
|
const errorMessage =
|
|
120
183
|
tabName === 'QC'
|
|
121
184
|
? 'No items pending QC verification'
|
|
122
185
|
: tabName === 'LOADING'
|
|
123
|
-
? 'No items
|
|
186
|
+
? 'No items found in this bin'
|
|
124
187
|
: 'No items pending dispatch'
|
|
125
188
|
throw new Error(errorMessage)
|
|
126
189
|
}
|
|
@@ -80,7 +80,7 @@ function formatDate(date: Date | null): string | null {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function shouldFilterBySortedQty(releaseGoodStatus: string, disableQC: boolean): boolean {
|
|
83
|
-
return releaseGoodStatus
|
|
83
|
+
return releaseGoodStatus === ORDER_STATUS.LOADING && !disableQC
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function getSortedQty(releaseGoodStatus: string, disableQC: boolean, pickedQty: number, sortedQty: number): number {
|
|
@@ -154,13 +154,19 @@ export const loadingWorksheetv2 = {
|
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
if (filter === FILTER_TYPES.QC) {
|
|
157
|
-
qb.andWhere('targetInventory.status
|
|
157
|
+
qb.andWhere('targetInventory.status IN (:...loadingStatus)', {
|
|
158
|
+
loadingStatus: [ORDER_INVENTORY_STATUS.SORTING, ORDER_INVENTORY_STATUS.LOADING]
|
|
159
|
+
})
|
|
158
160
|
qb.andWhere('targetInventory.sortedQty <> targetInventory.pickedQty')
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
if (filter === FILTER_TYPES.LOADING) {
|
|
162
164
|
if (shouldFilterBySortedQty(releaseGood.status, disableQc)) {
|
|
163
165
|
qb.andWhere('targetInventory.sortedQty > 0')
|
|
166
|
+
} else {
|
|
167
|
+
qb.andWhere('targetInventory.status IN (:...loadingStatus)', {
|
|
168
|
+
loadingStatus: [ORDER_INVENTORY_STATUS.SORTING, ORDER_INVENTORY_STATUS.LOADING]
|
|
169
|
+
})
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -232,6 +238,7 @@ async function mapdata(
|
|
|
232
238
|
id: wsd.targetInventory?.id,
|
|
233
239
|
batchId: wsd.targetInventory?.batchId || '-',
|
|
234
240
|
packingType: wsd.targetInventory?.packingType,
|
|
241
|
+
refWorksheetId: wsd.targetInventory?.refWorksheetId,
|
|
235
242
|
product: {
|
|
236
243
|
id: wsd.targetInventory?.product?.id,
|
|
237
244
|
sku: wsd.targetInventory?.product?.sku,
|