@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.
Files changed (78) hide show
  1. package/client/pages/inbound/unload-product.js +30 -33
  2. package/client/pages/outbound/loading-product.js +2 -3
  3. package/client/pages/outbound/loading-v2/loading-execution-base.js +568 -56
  4. package/client/pages/outbound/loading-v2/loading-execution.js +258 -8
  5. package/client/pages/outbound/loading-v2/loading.js +82 -12
  6. package/client/pages/outbound/picking-product.js +11 -6
  7. package/client/pages/outbound/picking-v2/picking-execution-base.js +8 -8
  8. package/client/pages/outbound/picking-v2/picking-execution.js +75 -0
  9. package/client/pages/outbound/route-label-history-dialog.js +253 -0
  10. package/client/pages/outbound/route-label-popup.js +637 -131
  11. package/client/pages/outbound/show-ro-list-popup.js +8 -4
  12. package/client/pages/outbound/single-outbound-worksheet.js +55 -6
  13. package/client/pages/outbound/sorting-product.js +71 -25
  14. package/client/pages/outbound/zone-worksheet.js +62 -3
  15. package/dist-server/entities/index.js +9 -0
  16. package/dist-server/entities/index.js.map +1 -0
  17. package/dist-server/entities/route-label-sequence-counter.js +75 -0
  18. package/dist-server/entities/route-label-sequence-counter.js.map +1 -0
  19. package/dist-server/entities/route-label-sequence-log.js +74 -0
  20. package/dist-server/entities/route-label-sequence-log.js.map +1 -0
  21. package/dist-server/graphql/resolvers/index.js +3 -0
  22. package/dist-server/graphql/resolvers/index.js.map +1 -1
  23. package/dist-server/graphql/resolvers/outbound/complete-load.js +40 -3
  24. package/dist-server/graphql/resolvers/outbound/complete-load.js.map +1 -1
  25. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +50 -11
  26. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js.map +1 -1
  27. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js +90 -48
  28. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js.map +1 -1
  29. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +15 -7
  30. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
  31. package/dist-server/graphql/resolvers/outbound/sort-item.js +95 -16
  32. package/dist-server/graphql/resolvers/outbound/sort-item.js.map +1 -1
  33. package/dist-server/graphql/resolvers/route-label/index.js +7 -0
  34. package/dist-server/graphql/resolvers/route-label/index.js.map +1 -0
  35. package/dist-server/graphql/resolvers/route-label/route-label.js +220 -0
  36. package/dist-server/graphql/resolvers/route-label/route-label.js.map +1 -0
  37. package/dist-server/graphql/resolvers/zone-worksheet/index.js.map +1 -1
  38. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js +123 -27
  39. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js.map +1 -1
  40. package/dist-server/graphql/resolvers/zone-worksheet/zone-worksheet.js.map +1 -1
  41. package/dist-server/graphql/types/index.js +4 -0
  42. package/dist-server/graphql/types/index.js.map +1 -1
  43. package/dist-server/graphql/types/outbound/index.js +7 -1
  44. package/dist-server/graphql/types/outbound/index.js.map +1 -1
  45. package/dist-server/graphql/types/route-label/index.js +14 -0
  46. package/dist-server/graphql/types/route-label/index.js.map +1 -0
  47. package/dist-server/graphql/types/route-label/route-label-sequence.js +53 -0
  48. package/dist-server/graphql/types/route-label/route-label-sequence.js.map +1 -0
  49. package/dist-server/index.js +5 -0
  50. package/dist-server/index.js.map +1 -1
  51. package/dist-server/validators/setting-validators.js +519 -0
  52. package/dist-server/validators/setting-validators.js.map +1 -0
  53. package/helps/operato-wms/outbound/picking-product.en.md +156 -158
  54. package/package.json +50 -50
  55. package/server/entities/index.ts +6 -0
  56. package/server/entities/route-label-sequence-counter.ts +56 -0
  57. package/server/entities/route-label-sequence-log.ts +47 -0
  58. package/server/graphql/resolvers/index.ts +3 -0
  59. package/server/graphql/resolvers/outbound/complete-load.ts +42 -1
  60. package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +56 -13
  61. package/server/graphql/resolvers/outbound/find-loadable-release-good.ts +120 -57
  62. package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +9 -2
  63. package/server/graphql/resolvers/outbound/sort-item.ts +125 -21
  64. package/server/graphql/resolvers/route-label/index.ts +16 -0
  65. package/server/graphql/resolvers/route-label/route-label.ts +264 -0
  66. package/server/graphql/resolvers/zone-worksheet/index.ts +2 -2
  67. package/server/graphql/resolvers/zone-worksheet/zone-picking.ts +156 -39
  68. package/server/graphql/resolvers/zone-worksheet/zone-worksheet.ts +1 -2
  69. package/server/graphql/types/index.ts +4 -0
  70. package/server/graphql/types/outbound/index.ts +7 -1
  71. package/server/graphql/types/route-label/index.ts +18 -0
  72. package/server/graphql/types/route-label/route-label-sequence.ts +50 -0
  73. package/server/index.ts +7 -1
  74. package/server/validators/setting-validators.ts +668 -0
  75. package/translations/en.json +280 -252
  76. package/translations/ko.json +29 -1
  77. package/translations/ms.json +28 -0
  78. 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 by bin location
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 = (tx: EntityManager, domainId: string, locationId: string) => {
37
- return tx
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
- .andWhere('orderInventory.bin_location_id = :locationId', { locationId })
54
- .orderBy('orderInventory.createdAt', 'ASC')
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: null,
77
- binLocationName: uniqueBinLocations.join(', ') // Join multiple bin locations with comma
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
- throw new OutboundOrderError(
136
- 'You are scanning using order number. Your setting for QC by BIN is enabled. Please contact your administrator.',
137
- 'QC_BY_BIN_ENABLED'
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: [ORDER_STATUS.LOADING, ORDER_STATUS.PROCESSING, ORDER_STATUS.PARTIAL_PROCESSING, ORDER_STATUS.DONE]
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 for single release good
29
- const counts = await tx
30
- .getRepository(OrderInventory)
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
- // First get all matching release goods
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
- .getRepository(OrderInventory)
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 pending loading'
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 !== ORDER_STATUS.LOADING || (releaseGoodStatus === ORDER_STATUS.LOADING && !disableQC)
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 = :loadingStatus', { loadingStatus: ORDER_INVENTORY_STATUS.SORTING })
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,