@things-factory/warehouse-base 8.0.0-beta.9 → 8.0.0

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 (99) hide show
  1. package/dist-server/service/inventory/inventory-mutation.js +15 -4
  2. package/dist-server/service/inventory/inventory-mutation.js.map +1 -1
  3. package/dist-server/service/inventory/inventory-query.d.ts +2 -0
  4. package/dist-server/service/inventory/inventory-types.d.ts +4 -0
  5. package/dist-server/service/inventory/inventory-types.js +16 -0
  6. package/dist-server/service/inventory/inventory-types.js.map +1 -1
  7. package/dist-server/service/inventory/inventory.d.ts +2 -0
  8. package/dist-server/service/inventory/inventory.js +12 -0
  9. package/dist-server/service/inventory/inventory.js.map +1 -1
  10. package/dist-server/service/warehouse/warehouse-mutation.js +24 -0
  11. package/dist-server/service/warehouse/warehouse-mutation.js.map +1 -1
  12. package/dist-server/tsconfig.tsbuildinfo +1 -1
  13. package/package.json +8 -8
  14. package/server/constants/index.ts +5 -0
  15. package/server/constants/inventory.ts +67 -0
  16. package/server/constants/location.ts +14 -0
  17. package/server/constants/pallet.ts +10 -0
  18. package/server/constants/rule-type.ts +5 -0
  19. package/server/constants/tote.ts +5 -0
  20. package/server/controllers/ecommerce/ecommerce-controller.ts +108 -0
  21. package/server/controllers/ecommerce/index.ts +2 -0
  22. package/server/controllers/ecommerce/sellercraft-controller.ts +100 -0
  23. package/server/controllers/index.ts +2 -0
  24. package/server/controllers/warehouse-controller.ts +181 -0
  25. package/server/index.ts +9 -0
  26. package/server/middlewares/index.ts +0 -0
  27. package/server/migrations/index.ts +9 -0
  28. package/server/service/index.ts +80 -0
  29. package/server/service/inventory/index.ts +6 -0
  30. package/server/service/inventory/inventory-mutation.ts +530 -0
  31. package/server/service/inventory/inventory-query.ts +1259 -0
  32. package/server/service/inventory/inventory-types.ts +367 -0
  33. package/server/service/inventory/inventory.ts +408 -0
  34. package/server/service/inventory-change/index.ts +6 -0
  35. package/server/service/inventory-change/inventory-change-mutation.ts +969 -0
  36. package/server/service/inventory-change/inventory-change-query.ts +93 -0
  37. package/server/service/inventory-change/inventory-change-types.ts +36 -0
  38. package/server/service/inventory-change/inventory-change.ts +164 -0
  39. package/server/service/inventory-history/index.ts +6 -0
  40. package/server/service/inventory-history/inventory-history-mutation.ts +116 -0
  41. package/server/service/inventory-history/inventory-history-query.ts +1845 -0
  42. package/server/service/inventory-history/inventory-history-types.ts +444 -0
  43. package/server/service/inventory-history/inventory-history.ts +203 -0
  44. package/server/service/inventory-item/index.ts +6 -0
  45. package/server/service/inventory-item/inventory-item-mutation.ts +217 -0
  46. package/server/service/inventory-item/inventory-item-query.ts +226 -0
  47. package/server/service/inventory-item/inventory-item-type.ts +74 -0
  48. package/server/service/inventory-item/inventory-item.ts +105 -0
  49. package/server/service/inventory-item-change/index.ts +6 -0
  50. package/server/service/inventory-item-change/inventory-item-change-mutation.ts +119 -0
  51. package/server/service/inventory-item-change/inventory-item-change-query.ts +47 -0
  52. package/server/service/inventory-item-change/inventory-item-change-type.ts +68 -0
  53. package/server/service/inventory-item-change/inventory-item-change.ts +92 -0
  54. package/server/service/inventory-product/index.ts +6 -0
  55. package/server/service/inventory-product/inventory-product-mutation.ts +116 -0
  56. package/server/service/inventory-product/inventory-product-query.ts +47 -0
  57. package/server/service/inventory-product/inventory-product-type.ts +59 -0
  58. package/server/service/inventory-product/inventory-product.ts +88 -0
  59. package/server/service/location/index.ts +6 -0
  60. package/server/service/location/location-mutation.ts +134 -0
  61. package/server/service/location/location-query.ts +244 -0
  62. package/server/service/location/location-types.ts +173 -0
  63. package/server/service/location/location.ts +121 -0
  64. package/server/service/movement/index.ts +6 -0
  65. package/server/service/movement/movement-mutation.ts +60 -0
  66. package/server/service/movement/movement-query.ts +263 -0
  67. package/server/service/movement/movement-types.ts +74 -0
  68. package/server/service/movement/movement.ts +81 -0
  69. package/server/service/pallet/index.ts +6 -0
  70. package/server/service/pallet/pallet-mutation.ts +242 -0
  71. package/server/service/pallet/pallet-query.ts +106 -0
  72. package/server/service/pallet/pallet-types.ts +80 -0
  73. package/server/service/pallet/pallet.ts +92 -0
  74. package/server/service/pallet-count/index.ts +6 -0
  75. package/server/service/pallet-count/pallet-count-mutation.ts +151 -0
  76. package/server/service/pallet-count/pallet-count-query.ts +45 -0
  77. package/server/service/pallet-count/pallet-count-types.ts +36 -0
  78. package/server/service/pallet-count/pallet-count.ts +70 -0
  79. package/server/service/pallet-history/index.ts +6 -0
  80. package/server/service/pallet-history/pallet-history-mutation.ts +114 -0
  81. package/server/service/pallet-history/pallet-history-query.ts +48 -0
  82. package/server/service/pallet-history/pallet-history-types.ts +36 -0
  83. package/server/service/pallet-history/pallet-history.ts +89 -0
  84. package/server/service/reduced-inventory-history/index.ts +3 -0
  85. package/server/service/reduced-inventory-history/reduced-inventory-history.ts +92 -0
  86. package/server/service/tote/index.ts +6 -0
  87. package/server/service/tote/tote-mutation.ts +201 -0
  88. package/server/service/tote/tote-query.ts +106 -0
  89. package/server/service/tote/tote-types.ts +44 -0
  90. package/server/service/tote/tote.ts +77 -0
  91. package/server/service/warehouse/index.ts +6 -0
  92. package/server/service/warehouse/warehouse-mutation.ts +152 -0
  93. package/server/service/warehouse/warehouse-query.ts +58 -0
  94. package/server/service/warehouse/warehouse-types.ts +50 -0
  95. package/server/service/warehouse/warehouse.ts +95 -0
  96. package/server/utils/datetime-util.ts +54 -0
  97. package/server/utils/index.ts +3 -0
  98. package/server/utils/inventory-no-generator.ts +15 -0
  99. package/server/utils/inventory-util.ts +490 -0
@@ -0,0 +1,1259 @@
1
+ import { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'
2
+ import { Brackets, EntityManager, Equal, Not, OrderByCondition, Repository, SelectQueryBuilder } from 'typeorm'
3
+
4
+ import { User } from '@things-factory/auth-base'
5
+ import { Bizplace, getPartnersCompanyBizplaces, getPermittedBizplaceIds } from '@things-factory/biz-base'
6
+ import { logger } from '@things-factory/env'
7
+ import { Product, ProductBundleSetting } from '@things-factory/product-base'
8
+ import { Setting } from '@things-factory/setting-base'
9
+ import {
10
+ buildQuery,
11
+ Domain,
12
+ Filter,
13
+ getQueryBuilderFromListParams,
14
+ getRepository,
15
+ ListParam,
16
+ Pagination,
17
+ Sorting
18
+ } from '@things-factory/shell'
19
+
20
+ import { INVENTORY_STATUS, LOCATION_TYPE } from '../../constants'
21
+ import { InventoryChange } from '../inventory-change/inventory-change'
22
+ import { Inventory } from './inventory'
23
+ import { InventoryBundleGroupDetail, InventoryList } from './inventory-types'
24
+
25
+ @Resolver(Inventory)
26
+ export class InventoryQuery {
27
+ /**
28
+ * Combined single query resolver to perform extraction of data with or without pagination
29
+ * @param context
30
+ * @param filters
31
+ * @param pagination
32
+ * @param sortings
33
+ * @param locationSortingRules
34
+ * @param exportItem
35
+ * @returns
36
+ */
37
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
38
+ @Directive('@transaction')
39
+ @Query(returns => InventoryList)
40
+ async inventories(
41
+ @Ctx() context: ResolverContext,
42
+ @Arg('filters', type => [Filter], { nullable: true }) filters?: Filter[],
43
+ @Arg('pagination', type => Pagination, { nullable: true }) pagination?: Pagination,
44
+ @Arg('sortings', type => [Sorting], { nullable: true }) sortings?: Sorting[],
45
+ @Arg('locationSortingRules', type => [Sorting], { nullable: true }) locationSortingRules?: Sorting[],
46
+ @Arg('exportItem', type => Boolean, { nullable: true }) exportItem?: Boolean
47
+ ): Promise<InventoryList> {
48
+ const { domain, user, tx } = context.state
49
+ const { page, limit } = pagination || {}
50
+
51
+ try {
52
+ //Define special filters
53
+ const productFilters = filters.find((filter: any) => filter.name == 'productInfo')
54
+ const remainOnlyParam = filters.find((filter: any) => filter.name == 'remainOnly')
55
+ const bizplace = filters.find((filter: any) => filter.name === 'bizplace')
56
+
57
+ filters = filters.filter(x => ['productInfo', 'remainOnly'].indexOf(x.name) < 0)
58
+
59
+ const params = { filters, pagination }
60
+
61
+ const remainOnly: boolean = remainOnlyParam?.value || false
62
+
63
+ const qb: SelectQueryBuilder<Inventory> = getQueryBuilderFromListParams({
64
+ repository: tx.getRepository(Inventory),
65
+ params,
66
+ domain,
67
+ alias: 'inventory',
68
+ searchables: ['warehouse', 'product', 'batchId', 'location', 'palletId', 'lot']
69
+ })
70
+
71
+ qb.leftJoinAndSelect('inventory.bizplace', 'bizplace')
72
+ .leftJoinAndSelect('inventory.product', 'product')
73
+ .leftJoinAndSelect('product.productDetails', 'productDetail', 'productDetail.id = inventory.product_detail_id')
74
+ .leftJoinAndSelect('inventory.warehouse', 'warehouse')
75
+ .leftJoinAndSelect('inventory.location', 'location')
76
+ .leftJoinAndSelect('inventory.creator', 'creator')
77
+ .leftJoinAndSelect('inventory.updater', 'updater')
78
+
79
+ if (!bizplace) {
80
+ const bizplaces = await getPermittedBizplaceIds(domain, user)
81
+
82
+ qb.andWhere(
83
+ new Brackets(qb => {
84
+ qb.orWhere('inventory.bizplace_id IN (:...bizplaces)', { bizplaces })
85
+ })
86
+ )
87
+ }
88
+
89
+ // To get aggregated serial number in csv and total number of stored serial number
90
+ qb.leftJoinAndSelect(
91
+ subQuery => {
92
+ return subQuery
93
+ .select('inventoryItems.inventory_id', 'inventory_item_inventory_id')
94
+ .addSelect(`SUM(case when "inventoryItems"."status" = 'STORED' then 1 else 0 end)`, 'inventory_item_count')
95
+ .addSelect(`string_agg(inventoryItems.serial_number, ', ')`, 'serial_numbers')
96
+ .from('inventory_items', 'inventoryItems')
97
+ .where(`inventoryItems.domain_id = :domainId`, { domainId: domain.id })
98
+ .andWhere(`inventoryItems.status = :ivicStatus`, { ivicStatus: INVENTORY_STATUS.STORED })
99
+ .groupBy('inventoryItems.inventory_id')
100
+ },
101
+ 'inventoryItems',
102
+ '"inventoryItems"."inventory_item_inventory_id" = "inventory"."id"'
103
+ )
104
+
105
+ // To get inventory with remaining qty
106
+ if (remainOnly) {
107
+ qb.andWhere('inventory.qty > 0')
108
+ .andWhere('CASE WHEN inventory.lockedQty IS NULL THEN 0 ELSE inventory.lockedQty END >= 0')
109
+ .andWhere('inventory.qty - CASE WHEN inventory.lockedQty IS NULL THEN 0 ELSE inventory.lockedQty END > 0')
110
+ }
111
+
112
+ // Filter based on multiple product parameters and allow to search in csv format
113
+ if (productFilters) {
114
+ let productFilterValue = `%${productFilters.value.toLowerCase()}%`
115
+ qb.andWhere(qb => {
116
+ const subQuery = qb
117
+ .subQuery()
118
+ .select()
119
+ .from(Product, `products`)
120
+ .where(`products.id = Inventory.product_id`) // @chrislim Does the uppercase I in Inventory affect? I can see the rest are in lowercase i
121
+ .andWhere(
122
+ new Brackets(qb => {
123
+ qb.where('Lower(products.sku) LIKE :productInfo', { productInfo: productFilterValue })
124
+ .orWhere('Lower(products.name) LIKE :productInfo', { productInfo: productFilterValue })
125
+ .orWhere('Lower(products.description) LIKE :productInfo', { productInfo: productFilterValue })
126
+ .orWhere('Lower(products.brand) LIKE :productInfo', { productInfo: productFilterValue })
127
+ })
128
+ )
129
+ .getQuery()
130
+ return `EXISTS ${subQuery}`
131
+ })
132
+ }
133
+
134
+ // Apply sorting based on child data
135
+ if (sortings?.length !== 0) {
136
+ const arrChildSortData = ['bizplace', 'product', 'location', 'warehouse', 'zone']
137
+ const sort: OrderByCondition = (sortings || []).reduce(
138
+ (acc, sort) => ({
139
+ ...acc,
140
+ [arrChildSortData.indexOf(sort.name) >= 0 ? sort.name + '.name' : 'inventory.' + sort.name]: sort.desc
141
+ ? 'DESC'
142
+ : 'ASC'
143
+ }),
144
+ {}
145
+ )
146
+ qb.orderBy(sort)
147
+ }
148
+
149
+ if (locationSortingRules?.length > 0) {
150
+ locationSortingRules.forEach(rule => {
151
+ qb.addOrderBy(`location.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
152
+ })
153
+ }
154
+
155
+ // Fetch all row for exporting
156
+ if (exportItem != true && page && limit) {
157
+ qb.offset((page - 1) * limit).limit(limit)
158
+ }
159
+
160
+ let items = (await qb.getRawMany()).map(item => {
161
+ return {
162
+ ...new Inventory(item),
163
+ serialNumbers: item.serial_numbers
164
+ }
165
+ })
166
+ let total = await qb.getCount()
167
+
168
+ return {
169
+ items,
170
+ total
171
+ }
172
+ } catch (error) {
173
+ logger.error(`inventory-query[inventories]`, error)
174
+ throw error
175
+ }
176
+ }
177
+
178
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
179
+ @Query(returns => Inventory)
180
+ async inventory(@Arg('palletId') palletId: string, @Ctx() context: ResolverContext): Promise<Inventory> {
181
+ const { domain } = context.state
182
+
183
+ return await getRepository(Inventory).findOne({
184
+ where: { domain: { id: domain.id }, palletId },
185
+ relations: ['domain', 'bizplace', 'product', 'location', 'warehouse', 'creator', 'updater']
186
+ })
187
+ }
188
+
189
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
190
+ @Query(returns => Inventory)
191
+ async inventoryById(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Inventory> {
192
+ const { domain } = context.state
193
+
194
+ return await getRepository(Inventory).findOne({
195
+ where: { domain: { id: domain.id }, id },
196
+ relations: ['domain', 'bizplace', 'product', 'location', 'warehouse', 'creator', 'updater']
197
+ })
198
+ }
199
+
200
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
201
+ @Query(returns => Inventory)
202
+ async inventoryByPallet(@Arg('palletId') palletId: string, @Ctx() context: ResolverContext): Promise<Inventory> {
203
+ const { domain } = context.state
204
+
205
+ return await getRepository(Inventory).findOne({
206
+ where: { domain: { id: domain.id }, palletId, status: Not(Equal(INVENTORY_STATUS.TERMINATED)) },
207
+ relations: ['domain', 'bizplace', 'product', 'location', 'warehouse', 'creator', 'updater']
208
+ })
209
+ }
210
+
211
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
212
+ @Query(returns => InventoryList)
213
+ async renewInventoriesGroupByProduct(
214
+ @Args(type => ListParam) params: ListParam,
215
+ @Ctx() context: ResolverContext
216
+ ): Promise<InventoryList> {
217
+ const { domain } = context.state
218
+
219
+ const queryBuilder = getQueryBuilderFromListParams({
220
+ repository: getRepository(Inventory),
221
+ params,
222
+ alias: 'i',
223
+ domain,
224
+ searchables: ['productName', 'productSKU'],
225
+ filtersMap: {
226
+ productName: {
227
+ relationColumn: 'product',
228
+ columnName: 'name'
229
+ },
230
+ productSKU: {
231
+ relationColumn: 'product',
232
+ columnName: 'sku'
233
+ }
234
+ }
235
+ })
236
+
237
+ queryBuilder
238
+ .select('product.id', 'productId')
239
+ .addSelect('product.name', 'productName')
240
+ .addSelect('product.sku', 'productSKU')
241
+ .addSelect('product.type', 'productType')
242
+ .addSelect('product.primaryValue', 'primaryValue')
243
+ .addSelect('product.primaryUnit', 'primaryUnit')
244
+ .addSelect('product.auxUnit1', 'auxUnit1')
245
+ .addSelect('product.minQty', 'minQty')
246
+ .addSelect('product.maxQty', 'maxQty')
247
+ .addSelect('i.product_color', 'productColor')
248
+ .addSelect('i.product_quality', 'productQuality')
249
+ .addSelect('i.work_category', 'workCategory')
250
+ .addSelect('i.packing_type', 'packingType')
251
+ .addSelect('COALESCE(SUM(i.qty), 0)', 'qty')
252
+ .addSelect('COALESCE(SUM(i.locked_qty), 0)', 'lockedQty')
253
+ .addSelect('COALESCE(SUM(i.uom_value), 0)', 'uomValue')
254
+ .addSelect('COALESCE(SUM(i.locked_uom_value), 0)', 'lockedUomValue')
255
+ .innerJoin('i.product', 'product', 'product.deleted_at IS NULL')
256
+ .andWhere('i.status = :status', { status: INVENTORY_STATUS.STORED })
257
+ .andWhere('i.expiration_date >= now()')
258
+ .andWhere('product.domain_id = :domain', { domain: domain.id })
259
+ .groupBy('product.id')
260
+ .addGroupBy('i.product_color')
261
+ .addGroupBy('i.product_quality')
262
+ .addGroupBy('i.packing_type')
263
+ .addGroupBy('i.work_category')
264
+
265
+ queryBuilder.orderBy('product.sku', 'ASC')
266
+
267
+ const items = await queryBuilder.getRawMany()
268
+ return { items, total: items.length }
269
+ }
270
+
271
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
272
+ @Directive('@transaction')
273
+ @Query(returns => InventoryList)
274
+ async inventoriesByProduct(
275
+ @Args(type => ListParam) params: ListParam,
276
+ @Ctx() context: ResolverContext
277
+ ): Promise<InventoryList> {
278
+ try {
279
+ const { domain, user, tx } = context.state
280
+ let permittedBizplaceIds: string[] = await getPermittedBizplaceIds(domain, user)
281
+ const partnersCompanyBizplaces: Bizplace[] = await getPartnersCompanyBizplaces(domain, user)
282
+
283
+ const page = params.pagination.page
284
+ const limit = params.pagination.limit
285
+
286
+ let bizplaceFilter = params.filters.find(filter => filter.name == 'bizplace')
287
+ let productFilter = params.filters.find(filter => filter.name == 'product')
288
+ let packingTypeFilter = params.filters.find(filter => filter.name == 'packingType')
289
+ let availableStockFilter = params.filters.find(filter => filter.name == 'availableStock')
290
+ let lowStockFilter = params.filters.find(filter => filter.name == 'lowStock')
291
+ let overStockFilter = params.filters.find(filter => filter.name == 'overStock')
292
+ let quarantineStockFilter = params.filters.find(filter => filter.name == 'quarantineStock')
293
+ let reserveStockFilter = params.filters.find(filter => filter.name == 'reserveStock')
294
+ let productTypeFilter = params.filters.find(filter => filter.name == 'type')
295
+
296
+ if (bizplaceFilter) {
297
+ const bizplaceQueryBuilder: SelectQueryBuilder<Bizplace> = getRepository(Bizplace).createQueryBuilder('b')
298
+ bizplaceQueryBuilder
299
+ .innerJoin('companies', 'c2', ' c2.domain_id = b.domain_id')
300
+ .innerJoin('bizplaces', 'b2', 'b2.company_id = c2.id')
301
+ .where('b2.id = :bizplaceId', { bizplaceId: bizplaceFilter.value })
302
+
303
+ let bizplaceList = await bizplaceQueryBuilder.getMany()
304
+
305
+ permittedBizplaceIds = [
306
+ ...permittedBizplaceIds.filter(itm => itm == bizplaceFilter.value),
307
+ ...bizplaceList.map(bz => bz.id),
308
+ ...partnersCompanyBizplaces.map(biz => biz.id)
309
+ ]
310
+ }
311
+
312
+ let bizplaceQuery = ''
313
+ if (permittedBizplaceIds.length > 0) {
314
+ bizplaceQuery = `AND EXISTS (
315
+ SELECT * FROM (VALUES ${permittedBizplaceIds.map(id => `('${id}')`).join(',')})
316
+ AS bizFilter(bizplace_id)
317
+ WHERE bizFilter.bizplace_id::uuid = "Inventory"."bizplace_id"
318
+ )`
319
+ } else {
320
+ bizplaceQuery = `1 = 0`
321
+ }
322
+
323
+ let productQuery = ''
324
+ if (productFilter) {
325
+ let productValue = productFilter.value
326
+ .toLowerCase()
327
+ .split(',')
328
+ .map(prod => {
329
+ return "'%" + prod.trim().replace(/'/g, "''") + "%'"
330
+ })
331
+ .join(',')
332
+ productQuery = `AND (
333
+ Lower("Product"."name") LIKE ANY(ARRAY[${productValue}])
334
+ OR Lower("Product"."sku") LIKE ANY(ARRAY[${productValue}])
335
+ OR Lower("Product"."brand") LIKE ANY(ARRAY[${productValue}])
336
+ OR Lower("Product"."description") LIKE ANY(ARRAY[${productValue}])
337
+ OR Lower("ProductRef"."name") LIKE ANY(ARRAY[${productValue}])
338
+ OR Lower("ProductRef"."sku") LIKE ANY(ARRAY[${productValue}])
339
+ OR Lower("ProductRef"."description") LIKE ANY(ARRAY[${productValue}])
340
+ )`
341
+ }
342
+
343
+ if (productTypeFilter) {
344
+ let productTypeValue = productTypeFilter.value
345
+ .toLowerCase()
346
+ .split(',')
347
+ .map(prod => {
348
+ return "'%" + prod.trim().replace(/'/g, "''") + "%'"
349
+ })
350
+ .join(',')
351
+ productQuery =
352
+ productQuery +
353
+ ` AND (
354
+ Lower("Product"."type") LIKE ANY(ARRAY[${productTypeValue}])
355
+ )`
356
+ }
357
+
358
+ let qtyStockQuery = ''
359
+
360
+ let availableStockQuery = ''
361
+
362
+ if (reserveStockFilter?.value) {
363
+ qtyStockQuery += `\nAND SUM("reserveQty") > 0`
364
+ }
365
+
366
+ if (quarantineStockFilter?.value) {
367
+ qtyStockQuery += `\nAND SUM("quarantineQty") > 0`
368
+ }
369
+
370
+ if (availableStockFilter?.value) {
371
+ qtyStockQuery += ` AND SUM("availableQty") > 0`
372
+ }
373
+
374
+ if (availableStockFilter?.value) {
375
+ availableStockQuery = ` AND SUM("Inventory"."qty") > 0`
376
+ }
377
+
378
+ if (availableStockFilter?.value && quarantineStockFilter?.value) {
379
+ qtyStockQuery = ''
380
+ qtyStockQuery += `\nAND (SUM("availableQty") > 0 \n OR SUM("quarantineQty") > 0)`
381
+ }
382
+
383
+ if (availableStockFilter?.value && reserveStockFilter?.value) {
384
+ qtyStockQuery = ''
385
+ qtyStockQuery += `\nAND (SUM("availableQty") > 0 \n OR SUM("reserveQty") > 0)`
386
+ }
387
+
388
+ if (quarantineStockFilter?.value && reserveStockFilter?.value) {
389
+ qtyStockQuery = ''
390
+ qtyStockQuery += `\nAND (SUM("quarantineQty") > 0 \n OR SUM("reserveQty") > 0)`
391
+ }
392
+
393
+ let inventoryQuery = ''
394
+ if (packingTypeFilter) {
395
+ let packingTypeValue = packingTypeFilter.value
396
+ .toLowerCase()
397
+ .split(',')
398
+ .map(prod => {
399
+ return "'%" + prod.trim().replace(/'/g, "''") + "%'"
400
+ })
401
+ .join(',')
402
+
403
+ inventoryQuery =
404
+ inventoryQuery +
405
+ ` AND (
406
+ Lower("Inventory"."packing_type") LIKE ANY(ARRAY[${packingTypeValue}])
407
+ )`
408
+ }
409
+
410
+ let thresholdQuery = ''
411
+ if (lowStockFilter?.value && overStockFilter?.value) {
412
+ throw new Error('invalid filter combination')
413
+ } else if (lowStockFilter?.value && !overStockFilter?.value) {
414
+ thresholdQuery = `AND SUM(COALESCE("minQty", 0)) > SUM("availableQty")`
415
+ } else if (!lowStockFilter?.value && overStockFilter?.value) {
416
+ thresholdQuery = `AND SUM(COALESCE("maxQty", 0)) > 0 AND SUM(COALESCE("maxQty", 0)) < SUM("availableQty")`
417
+ }
418
+
419
+ await tx.query(
420
+ `
421
+ CREATE TEMP TABLE order_inventories_by_products AS (
422
+ SELECT "product_id", "packing_type", sum("release_qty") AS "total_release_qty" FROM order_inventories oi
423
+ WHERE "type" = 'RELEASE_OF_GOODS'
424
+ AND "domain_id" = $1
425
+ AND "inventory_id" ISNULL
426
+ AND "status" IN ('PENDING','PENDING_RECEIVE','PENDING_WORKSHEET','READY_TO_PICK'/*, 'PENDING_SPLIT' kiv sebab kalau pending split, ada yang dah assign tapi belum delete*/)
427
+ GROUP BY "product_id", "packing_type"
428
+ )`,
429
+ [domain.id]
430
+ )
431
+
432
+ await tx.query(
433
+ `
434
+ create temp table temp_inv_history AS
435
+ (
436
+ SELECT "Product"."id" AS "id", "Product"."sku" AS "sku", "Product"."brand" AS "brand", "Product"."name" AS "name", "Product"."type" AS "type", "Product"."description" AS "description",
437
+ "Inventory"."packing_type" AS "packingType",
438
+ "Product"."weight" AS "weight", "ProductRef"."id" AS "productRefId",
439
+ "ProductRef"."description" AS "productRefDesciption", "Bizplace"."id" AS "bizplaceId", "Bizplace"."name" AS "bizplaceName", SUM("Inventory"."qty") AS "qty" ,
440
+ CASE WHEN SUM("Inventory"."qty") > 0 THEN SUM(COALESCE("Inventory"."unit_cost", 0) * "Inventory"."qty")/ SUM("Inventory"."qty") ELSE 0 END AS "averageUnitCost",
441
+ COALESCE("ProductDetailBizplaceSetting"."min_qty", "Product"."min_qty",0) AS "minQty",
442
+ COALESCE("ProductDetailBizplaceSetting"."max_qty", "Product"."max_qty",0) AS "maxQty",
443
+ CASE WHEN "Location"."type" NOT IN ('${LOCATION_TYPE.QUARANTINE}','${LOCATION_TYPE.RESERVE}')
444
+ THEN
445
+ CASE WHEN SUM("Inventory"."qty") > 0
446
+ THEN SUM("Inventory"."qty")-SUM(COALESCE("Inventory"."locked_qty", 0) + COALESCE("OrderInventoriesByProduct"."total_release_qty", 0))
447
+ ELSE 0 END
448
+ ELSE 0 END AS "availableQty",
449
+ SUM(COALESCE("Inventory"."locked_qty", 0) + COALESCE("OrderInventoriesByProduct"."total_release_qty", 0)) AS "releaseQty",
450
+ CASE WHEN "Location"."type" = '${LOCATION_TYPE.QUARANTINE}'
451
+ THEN
452
+ CASE WHEN SUM("Inventory"."qty") > 0
453
+ THEN SUM("Inventory"."qty")-SUM(COALESCE("Inventory"."locked_qty", 0) + COALESCE("OrderInventoriesByProduct"."total_release_qty", 0))
454
+ ELSE 0 END
455
+ ELSE 0 END AS "quarantineQty",
456
+ CASE WHEN "Location"."type" = '${LOCATION_TYPE.RESERVE}'
457
+ THEN
458
+ CASE WHEN SUM("Inventory"."qty") > 0
459
+ THEN SUM("Inventory"."qty")-SUM(COALESCE("Inventory"."locked_qty", 0) + COALESCE("OrderInventoriesByProduct"."total_release_qty", 0))
460
+ ELSE 0 END
461
+ ELSE 0 END AS "reserveQty",
462
+ CASE WHEN "Location"."type" = '${LOCATION_TYPE.BIN}' THEN SUM("Inventory"."qty") ELSE 0 END AS "binQty"
463
+ FROM "inventories" "Inventory"
464
+ LEFT JOIN "products" "Product" ON "Product"."id"="Inventory"."product_id"
465
+ LEFT JOIN "product_details" "ProductDetails" ON "ProductDetails"."product_id" = "Product"."id"
466
+ AND "ProductDetails"."is_default"
467
+ LEFT JOIN "product_detail_bizplace_settings" "ProductDetailBizplaceSetting" ON "ProductDetailBizplaceSetting"."product_detail_id" = "ProductDetails"."id"
468
+ AND "ProductDetailBizplaceSetting"."domain_id" = "Inventory"."domain_id"
469
+ LEFT JOIN "products" "ProductRef" ON "ProductRef"."id"="Product"."product_ref_id"
470
+ INNER JOIN "bizplaces" "Bizplace" ON "Bizplace"."id"="Inventory"."bizplace_id"
471
+ INNER JOIN "locations" "Location" ON "Location"."id" = "Inventory"."location_id"
472
+ LEFT JOIN "order_inventories_by_products" "OrderInventoriesByProduct"
473
+ ON "OrderInventoriesByProduct"."product_id" = "Inventory"."product_id"
474
+ AND "OrderInventoriesByProduct"."packing_type" = "Inventory"."packing_type"
475
+ WHERE "Inventory"."qty" >= 0
476
+ AND "Inventory"."status" <> 'MISSING'
477
+ AND "Inventory"."domain_id" = $1
478
+ ${bizplaceQuery}
479
+ ${productQuery}
480
+ GROUP BY "Product"."id", "Bizplace"."id", "ProductDetailBizplaceSetting"."id", "Inventory"."packing_type", "Location"."type", "ProductRef"."id"
481
+ HAVING 1 = 1
482
+ ${availableStockQuery}
483
+ )`,
484
+ [domain.id]
485
+ )
486
+
487
+ await tx.query(
488
+ `
489
+ CREATE TEMP TABLE grouped_inventories_product AS (
490
+ SELECT "id", "sku", "brand", "name", "description",
491
+ "packingType", "bizplaceId", "bizplaceName", sum("averageUnitCost") AS "averageUnitCost",
492
+ sum("minQty") AS "minQty", sum("maxQty") AS "maxQty",
493
+ sum("availableQty") AS "availableQty",
494
+ sum("quarantineQty") AS "quarantineQty",
495
+ sum("reserveQty") AS "reserveQty",
496
+ sum("releaseQty") AS "releaseQty",
497
+ sum("availableQty" + "quarantineQty" + "releaseQty" + "reserveQty" - "binQty") AS "warehouseQty"
498
+ FROM temp_inv_history
499
+ GROUP BY "id", "sku", "brand", "name", "description", "packingType", "bizplaceId", "bizplaceName"
500
+ HAVING 1=1
501
+ ${qtyStockQuery}
502
+ ${thresholdQuery}
503
+ )
504
+ `
505
+ )
506
+ const results: any = await tx.query(
507
+ `
508
+ SELECT * FROM grouped_inventories_product
509
+ ORDER BY "bizplaceName", "sku"
510
+ OFFSET $1 LIMIT $2
511
+ `,
512
+ [(page - 1) * limit, limit]
513
+ )
514
+
515
+ const total: any = await tx.query(`SELECT COUNT(*) FROM grouped_inventories_product`)
516
+
517
+ await tx.query(`drop table temp_inv_history, order_inventories_by_products, grouped_inventories_product`)
518
+
519
+ return {
520
+ items: results.map((item: any) => {
521
+ return {
522
+ product: {
523
+ id: item.id,
524
+ name: item.name,
525
+ sku: item.sku,
526
+ brand: item.brand,
527
+ description: item.description,
528
+ minQty: item.minQty,
529
+ maxQty: item.maxQty
530
+ },
531
+ bizplace: {
532
+ id: item.bizplaceId,
533
+ name: item.bizplaceName
534
+ },
535
+ packingType: item.packingType,
536
+ averageUnitCost: item.averageUnitCost,
537
+ availableQty: item.availableQty,
538
+ releaseQty: item.releaseQty,
539
+ quarantineQty: item.quarantineQty,
540
+ reserveQty: item.reserveQty,
541
+ warehouseQty: item.warehouseQty
542
+ }
543
+ }),
544
+ total: total[0].count
545
+ }
546
+ } catch (error) {
547
+ throw error
548
+ }
549
+ }
550
+
551
+ @Directive('@transaction')
552
+ @Query(returns => InventoryList)
553
+ async inventoriesByStrategy(
554
+ @Ctx() context: ResolverContext,
555
+ @Arg('worksheetId') worksheetId: string,
556
+ @Arg('batchId') batchId: string,
557
+ @Arg('productName') productName: string,
558
+ @Arg('productSku') productSku: string,
559
+ @Arg('packingType') packingType: string,
560
+ @Arg('packingSize') packingSize: number,
561
+ @Arg('uom') uom: string,
562
+ @Arg('pickingStrategy') pickingStrategy: string,
563
+ @Arg('locationSortingRules', type => [Sorting], { nullable: true }) locationSortingRules?: Sorting[],
564
+ @Arg('bizplaceId', { nullable: true }) bizplaceId?: string
565
+ ): Promise<InventoryList> {
566
+ const { domain, tx } = context.state
567
+
568
+ const inventoryAssignmentSetting: Setting = await tx.getRepository(Setting).findOne({
569
+ where: { domain: { id: domain.id }, name: 'rule-for-inventory-assignment' }
570
+ })
571
+
572
+ if (!locationSortingRules && inventoryAssignmentSetting) {
573
+ locationSortingRules = []
574
+ let locationSetting = JSON.parse(inventoryAssignmentSetting.value)
575
+ for (const key in locationSetting) {
576
+ locationSortingRules.push({ name: key, desc: locationSetting[key] == 'ASC' ? false : true })
577
+ }
578
+ }
579
+
580
+ return inventoriesByStrategy(
581
+ {
582
+ worksheetId,
583
+ batchId,
584
+ bizplaceId,
585
+ productName,
586
+ productSku,
587
+ packingType,
588
+ packingSize,
589
+ uom,
590
+ pickingStrategy,
591
+ locationSortingRules
592
+ },
593
+ tx
594
+ )
595
+ }
596
+
597
+ @Directive('@transaction')
598
+ @Query(returns => Boolean)
599
+ async checkProductIdenticality(
600
+ @Arg('palletA') palletA: string,
601
+ @Arg('palletB') palletB: string,
602
+ @Ctx() context: ResolverContext
603
+ ): Promise<Boolean> {
604
+ const { tx } = context.state
605
+ const invRepo: Repository<Inventory> = tx.getRepository(Inventory)
606
+ const invA: Inventory = await invRepo.findOne({
607
+ where: { domain: { id: context.state.domain.id }, palletId: palletA, status: INVENTORY_STATUS.STORED },
608
+ relations: ['product']
609
+ })
610
+
611
+ const invB: Inventory = await invRepo.findOne({
612
+ where: { domain: { id: context.state.domain.id }, palletId: palletB, status: INVENTORY_STATUS.STORED },
613
+ relations: ['product']
614
+ })
615
+
616
+ return (
617
+ invA?.batchId === invB?.batchId &&
618
+ invA?.product?.id === invB?.product?.id &&
619
+ invA?.packingType === invB?.packingType
620
+ )
621
+ }
622
+
623
+ @Query(returns => Boolean)
624
+ async checkCartonIdenticality(
625
+ @Arg('cartonA') cartonA: string,
626
+ @Arg('palletA') palletA: string,
627
+ @Arg('cartonB') cartonB: string,
628
+ @Ctx() context: ResolverContext
629
+ ) {
630
+ const invRepo: Repository<Inventory> = getRepository(Inventory)
631
+ const invA: Inventory = await invRepo.findOne({
632
+ where: {
633
+ domain: { id: context.state.domain.id },
634
+ palletId: palletA,
635
+ cartonId: cartonA,
636
+ status: INVENTORY_STATUS.STORED
637
+ },
638
+ relations: ['product']
639
+ })
640
+
641
+ const invB: Inventory = await invRepo.findOne({
642
+ where: {
643
+ domain: { id: context.state.domain.id },
644
+ packingType: invA.packingType,
645
+ packingSize: invA.packingSize,
646
+ product: { id: invA.product.id },
647
+ cartonId: cartonB,
648
+ status: INVENTORY_STATUS.STORED
649
+ },
650
+ relations: ['product']
651
+ })
652
+
653
+ return (
654
+ invA?.expirationDate === invB?.expirationDate &&
655
+ invA?.product?.id === invB?.product?.id &&
656
+ invA?.packingType === invB?.packingType &&
657
+ invA?.packingSize === invB?.packingSize
658
+ )
659
+ }
660
+
661
+ @Query(returns => Boolean)
662
+ async checkInventoryOwner(
663
+ @Arg('palletId') palletId: string,
664
+ @Arg('bizplaceName') bizplaceName: string,
665
+ @Ctx() context: ResolverContext
666
+ ): Promise<Boolean> {
667
+ const invRepo: Repository<Inventory> = getRepository(Inventory)
668
+ const bizRepo: Repository<Bizplace> = getRepository(Bizplace)
669
+
670
+ const inventory: Inventory = await invRepo.findOne({
671
+ where: { domain: { id: context.state.domain.id }, palletId, status: INVENTORY_STATUS.STORED },
672
+ relations: ['bizplace']
673
+ })
674
+
675
+ if (!inventory) throw new Error('This inventory status is not stored.')
676
+
677
+ const ownerBizplace: Bizplace = await bizRepo.findOne({
678
+ where: { name: bizplaceName }
679
+ })
680
+
681
+ const foundBizplace: Bizplace = inventory.bizplace
682
+
683
+ return Boolean(ownerBizplace.id === foundBizplace.id)
684
+ }
685
+
686
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
687
+ @Query(returns => InventoryBundleGroupDetail)
688
+ async inventoriesByBundle(
689
+ @Ctx() context: ResolverContext,
690
+ @Arg('filters', type => [Filter], { nullable: true }) filters?: Filter[],
691
+ @Arg('pagination', type => Pagination, { nullable: true }) pagination?: Pagination,
692
+ @Arg('sortings', type => [Sorting], { nullable: true }) sortings?: Sorting[]
693
+ ): Promise<InventoryBundleGroupDetail> {
694
+ const { domain, user } = context.state
695
+
696
+ const params = { filters, pagination }
697
+
698
+ const productBundleId = params.filters.filter(x => x.name === 'productBundleId')[0].value
699
+ if (!productBundleId) {
700
+ throw new Error('params product bundle is missing')
701
+ }
702
+
703
+ const bundleReleaseQty: number = params.filters.filter(x => x.name === 'bundleReleaseQty')[0].value
704
+
705
+ // remove productBundleId and bundleReleaseQty from params.filters
706
+ params.filters = params.filters.filter(x => x.name !== 'productBundleId' && x.name !== 'bundleReleaseQty')
707
+
708
+ if (!params.filters.find((filter: any) => filter.name === 'bizplace_id')) {
709
+ params.filters.push({
710
+ name: 'bizplace_id',
711
+ operator: 'in',
712
+ value: await getPermittedBizplaceIds(domain, user),
713
+ relation: true
714
+ })
715
+ }
716
+
717
+ const remainOnlyParam = params?.filters?.find(
718
+ (f: { name: string; operator: string; value: any }) => f.name === 'remainOnly'
719
+ )
720
+
721
+ let remainOnly: boolean = false
722
+ if (typeof remainOnlyParam?.value !== 'undefined') {
723
+ remainOnly = remainOnlyParam.value
724
+ params.filters = params.filters.filter(
725
+ (f: { name: string; operator: string; value: any }) => f.name !== 'remainOnly'
726
+ )
727
+ }
728
+
729
+ const unlockOnlyParam = params?.filters?.find(
730
+ (f: { name: string; operator: string; value: any }) => f.name === 'unlockOnly'
731
+ )
732
+
733
+ let unlockOnly: boolean = false
734
+ if (typeof unlockOnlyParam?.value !== 'undefined') {
735
+ unlockOnly = unlockOnlyParam.value
736
+ params.filters = params.filters.filter(
737
+ (f: { name: string; operator: string; value: any }) => f.name !== 'unlockOnly'
738
+ )
739
+ }
740
+
741
+ const productBundleSettings: ProductBundleSetting[] = await getRepository(ProductBundleSetting).find({
742
+ where: { productBundle: { id: productBundleId } },
743
+ relations: ['product', 'productBundle']
744
+ })
745
+
746
+ if (!productBundleSettings.length) {
747
+ throw new Error('product bundle settings is not found')
748
+ }
749
+
750
+ const qb: SelectQueryBuilder<Inventory> = getRepository(Inventory).createQueryBuilder('iv')
751
+ buildQuery(qb, params, context)
752
+
753
+ qb.select('iv.product_id', 'productId')
754
+ .addSelect('iv.batch_id', 'batchId')
755
+ .addSelect('iv.batch_id_ref', 'batchIdRef')
756
+ .addSelect('iv.packing_type', 'packingType')
757
+ .addSelect('SUM(iv.qty)', 'qty')
758
+ .addSelect('SUM(iv.locked_qty)', 'lockedQty')
759
+ .addSelect('SUM(iv.uom_value)', 'uomValue')
760
+ .addSelect('SUM(iv.locked_uom_value)', 'lockedUomValue')
761
+ .addSelect('prd.sku', 'productSku')
762
+ .addSelect('prd.name', 'productName')
763
+ .addSelect('prd.description', 'productDescription')
764
+ .addSelect('prd.type', 'productType')
765
+ .addSelect('prd.primary_unit', 'uom')
766
+ .leftJoin('iv.product', 'prd')
767
+
768
+ if (remainOnly) {
769
+ qb.andWhere('iv.qty > 0')
770
+ .andWhere('CASE WHEN iv.lockedQty IS NULL THEN 0 ELSE iv.lockedQty END >= 0')
771
+ .andWhere('iv.qty - CASE WHEN iv.lockedQty IS NULL THEN 0 ELSE iv.lockedQty END > 0')
772
+ }
773
+
774
+ if (unlockOnly) {
775
+ qb.andWhere('CASE WHEN iv.lockedQty IS NULL THEN 0 ELSE iv.lockedQty END = 0')
776
+ }
777
+
778
+ qb.andWhere('iv.product_id IN (:...productIds)', {
779
+ productIds: productBundleSettings.map(productBundle => productBundle.product.id)
780
+ })
781
+ .groupBy('iv.product_id')
782
+ .addGroupBy('iv.batch_id')
783
+ .addGroupBy('iv.batch_id_ref')
784
+ .addGroupBy('iv.packing_type')
785
+ .addGroupBy('prd.sku')
786
+ .addGroupBy('prd.name')
787
+ .addGroupBy('prd.description')
788
+ .addGroupBy('prd.type')
789
+ .addOrderBy('prd.name')
790
+ .addGroupBy('prd.primary_unit')
791
+ .addOrderBy('iv.batch_id')
792
+
793
+ let bundleGroup = await qb.getRawMany()
794
+
795
+ bundleGroup = bundleGroup.map((item: any) => {
796
+ return {
797
+ ...item,
798
+ bundleId: productBundleId,
799
+ remainQty: item.qty - (item.lockedQty ? item.lockedQty : 0),
800
+ remainUomValue: item.uomValue - (item.lockedUomValue ? item.lockedUomValue : 0),
801
+ uom: item.uom,
802
+ bundleQty: productBundleSettings.filter(pbs => pbs.product.id === item.productId)[0].bundleQty
803
+ }
804
+ })
805
+
806
+ const bundleSetting = productBundleSettings.map(pbs => {
807
+ return {
808
+ id: pbs.id,
809
+ productId: pbs.product.id,
810
+ bundleId: pbs.productBundle.id,
811
+ bundleQty: pbs.bundleQty,
812
+ releaseQty: bundleReleaseQty * pbs.bundleQty,
813
+ productReleaseQty: 0,
814
+ productReleaseUomValue: 0
815
+ }
816
+ })
817
+
818
+ /** ************************ **/
819
+ bundleGroup = bundleGroup
820
+ .map(group => {
821
+ const uomValue = group.remainUomValue / group.remainQty
822
+
823
+ bundleSetting.forEach(setting => {
824
+ let assigned = false
825
+ let releaseQty = 0
826
+ let releaseUomValue = 0
827
+
828
+ if (group.productId === setting.productId) {
829
+ if (setting.productReleaseQty < setting.releaseQty) {
830
+ assigned = true
831
+ const leftQty = setting.releaseQty - setting.productReleaseQty
832
+ releaseQty = leftQty > group.remainQty ? group.remainQty : leftQty
833
+ releaseUomValue = Math.round(releaseQty * uomValue * 100) / 100
834
+ setting.productReleaseQty += releaseQty
835
+ setting.productReleaseUomValue += releaseUomValue
836
+ }
837
+
838
+ group.assigned = assigned
839
+ group.releaseQty = releaseQty
840
+ group.releaseUomValue = releaseUomValue
841
+ group.releaseUomValueWithUom = `${releaseUomValue} ${group.uom}`
842
+ }
843
+ })
844
+
845
+ return group
846
+ })
847
+ .filter(group => group.assigned)
848
+ /** ************************ **/
849
+
850
+ return { bundleGroup, bundleSetting }
851
+ }
852
+
853
+ @Directive('@privilege(category: "inventory", privilege: "query", domainOwnerGranted: true)')
854
+ @Directive('@transaction')
855
+ @Query(returns => InventoryList)
856
+ async inventoriesGroupByProduct(
857
+ @Args(type => ListParam) params: ListParam,
858
+ @Ctx() context: ResolverContext
859
+ ): Promise<InventoryList> {
860
+ try {
861
+ const { domain, user, tx } = context.state
862
+ let permittedBizplaceIds: string[] = await getPermittedBizplaceIds(domain, user)
863
+ const partnersCompanyBizplaces: Bizplace[] = await getPartnersCompanyBizplaces(domain, user)
864
+
865
+ const page = params.pagination.page
866
+ const limit = params.pagination.limit
867
+
868
+ let bizplaceFilter = params.filters.find(filter => filter.name == 'bizplace')
869
+ let productFilter = params.filters.find(filter => filter.name == 'product')
870
+ let productTypeFilter = params.filters.find(filter => filter.name == 'type')
871
+ let availableStockFilter = params.filters.find(filter => filter.name == 'availableStock')
872
+ let lowStockFilter = params.filters.find(filter => filter.name == 'lowStock')
873
+ let overStockFilter = params.filters.find(filter => filter.name == 'overStock')
874
+ let warehouseNameFilter = params.filters.find(filter => filter.name == 'warehouseName')
875
+ let unexpiredOnlyFilter = params.filters.find(filter => filter.name === 'unexpiredOnly')
876
+
877
+ if (bizplaceFilter) {
878
+ const bizplaceQueryBuilder: SelectQueryBuilder<Bizplace> = getRepository(Bizplace).createQueryBuilder('b')
879
+ bizplaceQueryBuilder
880
+ .innerJoin('companies', 'c2', ' c2.domain_id = b.domain_id')
881
+ .innerJoin('bizplaces', 'b2', 'b2.company_id = c2.id')
882
+ .where('b2.id = :bizplaceId', { bizplaceId: bizplaceFilter.value })
883
+
884
+ let bizplaceList = await bizplaceQueryBuilder.getMany()
885
+
886
+ permittedBizplaceIds = [
887
+ ...permittedBizplaceIds.filter(itm => itm == bizplaceFilter.value),
888
+ ...bizplaceList.map(bz => bz.id),
889
+ ...partnersCompanyBizplaces.map(biz => biz.id)
890
+ ]
891
+ }
892
+
893
+ let bizplaceQuery = ''
894
+ if (permittedBizplaceIds.length > 0) {
895
+ bizplaceQuery = `AND EXISTS (
896
+ SELECT * FROM (VALUES ${permittedBizplaceIds.map(id => `('${id}')`).join(',')})
897
+ AS bizFilter(bizplace_id)
898
+ WHERE bizFilter.bizplace_id::uuid = "Inventory"."bizplace_id"
899
+ )`
900
+ } else {
901
+ bizplaceQuery = `1 = 0`
902
+ }
903
+
904
+ let productQuery = ''
905
+ if (productFilter) {
906
+ let productValue = productFilter.value
907
+ .toLowerCase()
908
+ .split(',')
909
+ .map(prod => {
910
+ return "'%" + prod.trim().replace(/'/g, "''") + "%'"
911
+ })
912
+ .join(',')
913
+ productQuery = `AND (
914
+ Lower("Product"."name") LIKE ANY(ARRAY[${productValue}])
915
+ OR Lower("Product"."sku") LIKE ANY(ARRAY[${productValue}])
916
+ OR Lower("Product"."brand") LIKE ANY(ARRAY[${productValue}])
917
+ OR Lower("Product"."description") LIKE ANY(ARRAY[${productValue}])
918
+ OR Lower("ProductRef"."name") LIKE ANY(ARRAY[${productValue}])
919
+ OR Lower("ProductRef"."sku") LIKE ANY(ARRAY[${productValue}])
920
+ OR Lower("ProductRef"."description") LIKE ANY(ARRAY[${productValue}])
921
+ )`
922
+ }
923
+
924
+ if (productTypeFilter) {
925
+ let productTypeValue = productTypeFilter.value
926
+ .toLowerCase()
927
+ .split(',')
928
+ .map(prod => {
929
+ return "'%" + prod.trim().replace(/'/g, "''") + "%'"
930
+ })
931
+ .join(',')
932
+ productQuery =
933
+ productQuery +
934
+ ` AND (
935
+ Lower("Product"."type") LIKE ANY(ARRAY[${productTypeValue}])
936
+ )`
937
+ }
938
+
939
+ let availableStockQuery = ''
940
+ if (availableStockFilter?.value) {
941
+ availableStockQuery = ` AND SUM("Inventory"."qty") > 0`
942
+ }
943
+
944
+ let unexpiredOnlyQuery = ''
945
+ if (unexpiredOnlyFilter?.value) {
946
+ unexpiredOnlyQuery = 'AND "Inventory"."expiration_date" >= CURRENT_DATE'
947
+ }
948
+
949
+ let thresholdQuery = ''
950
+ if (lowStockFilter?.value) {
951
+ thresholdQuery = `AND COALESCE("ProductDetailBizplaceSetting"."min_qty", "Product"."min_qty",0) > SUM("Inventory"."qty")`
952
+ }
953
+
954
+ if (overStockFilter?.value) {
955
+ thresholdQuery = `AND COALESCE("ProductDetailBizplaceSetting"."max_qty", "Product"."max_qty",0) > 0 AND
956
+ COALESCE("ProductDetailBizplaceSetting"."max_qty", "Product"."max_qty",0) < SUM("Inventory"."qty")`
957
+ }
958
+
959
+ let warehouseQuery = ''
960
+ if (warehouseNameFilter?.value) {
961
+ warehouseQuery = `AND "Warehouse"."name" like '%${warehouseNameFilter.value}%'`
962
+ }
963
+
964
+ await tx.query(
965
+ `
966
+ create temp table temp_inv_history AS
967
+ (
968
+ SELECT "Product"."id" AS "id", "Product"."sku" AS "sku", "Product"."brand" AS "brand", "Product"."name" AS "name", "Product"."type" AS "type", "Product"."description" AS "description", "Product"."primary_unit" AS "primaryUnit",
969
+ "Product"."weight" AS "weight", "ProductRef"."id" AS "productRefId",
970
+ "ProductRef"."description" AS "productRefDesciption", "Bizplace"."id" AS "bizplaceId", "Bizplace"."name" AS "bizplaceName", SUM("Inventory"."qty") AS "qty" , SUM("Inventory"."uom_value") AS "uomValue",
971
+ "Warehouse"."id" AS "warehouseId", "Warehouse"."name" AS "warehouseName",
972
+ CASE WHEN SUM("Inventory"."qty") > 0 THEN SUM(COALESCE("Inventory"."unit_cost", 0) * "Inventory"."qty")/ SUM("Inventory"."qty") ELSE 0 END AS "averageUnitCost",
973
+ COALESCE("ProductDetailBizplaceSetting"."min_qty", "Product"."min_qty",0) AS "minQty",
974
+ COALESCE("ProductDetailBizplaceSetting"."max_qty", "Product"."max_qty",0) AS "maxQty"
975
+ FROM "inventories" "Inventory"
976
+ LEFT JOIN "warehouses" "Warehouse" ON "Warehouse"."id"="Inventory"."warehouse_id"
977
+ LEFT JOIN "products" "Product" ON "Product"."id"="Inventory"."product_id"
978
+ LEFT JOIN "product_details" "ProductDetails" ON "ProductDetails"."product_id" = "Product"."id"
979
+ AND "ProductDetails"."is_default"
980
+ LEFT JOIN "product_detail_bizplace_settings" "ProductDetailBizplaceSetting" ON "ProductDetailBizplaceSetting"."product_detail_id" = "ProductDetails"."id"
981
+ AND "ProductDetailBizplaceSetting"."domain_id" = "Inventory"."domain_id"
982
+ LEFT JOIN "products" "ProductRef" ON "ProductRef"."id"="Product"."product_ref_id"
983
+ INNER JOIN "bizplaces" "Bizplace" ON "Bizplace"."id"="Inventory"."bizplace_id"
984
+ WHERE "Inventory"."qty" >= 0
985
+ AND "Inventory"."domain_id" = $1
986
+ ${bizplaceQuery}
987
+ ${productQuery}
988
+ ${warehouseQuery}
989
+ ${unexpiredOnlyQuery}
990
+ GROUP BY "Product"."id", "ProductRef"."id", "Bizplace"."id", "ProductDetailBizplaceSetting"."id", "Warehouse"."id"
991
+ HAVING 1 = 1
992
+ ${availableStockQuery}
993
+ ${thresholdQuery}
994
+ )`,
995
+ [domain.id]
996
+ )
997
+
998
+ const results: any = await tx.query(
999
+ `
1000
+ SELECT * FROM temp_inv_history
1001
+ ORDER BY "bizplaceName", "sku"
1002
+ OFFSET $1 LIMIT $2
1003
+ `,
1004
+ [(page - 1) * limit, limit]
1005
+ )
1006
+
1007
+ const total: any = await tx.query(`SELECT COUNT(*) FROM temp_inv_history`)
1008
+
1009
+ tx.query(`drop table temp_inv_history`)
1010
+
1011
+ return {
1012
+ items: results.map((item: any) => {
1013
+ return {
1014
+ product: {
1015
+ id: item.id,
1016
+ name: item.name,
1017
+ sku: item.sku,
1018
+ brand: item.brand,
1019
+ description: item.description,
1020
+ type: item.type,
1021
+ weight: item.weight,
1022
+ productRefId: item.productRefId,
1023
+ bizplaceId: item.bizplaceId,
1024
+ minQty: item.minQty,
1025
+ maxQty: item.maxQty,
1026
+ primaryUnit: item.primaryUnit
1027
+ },
1028
+ warehouse: {
1029
+ id: item.warehouseId,
1030
+ name: item.warehouseName
1031
+ },
1032
+ averageUnitCost: item.averageUnitCost,
1033
+ qty: item.qty,
1034
+ uomValue: item.uomValue
1035
+ }
1036
+ }),
1037
+ total: total[0].count
1038
+ }
1039
+ } catch (error) {
1040
+ throw error
1041
+ }
1042
+ }
1043
+
1044
+ @FieldResolver(type => Domain)
1045
+ async domain(@Root() inventory: Inventory): Promise<Domain> {
1046
+ return await getRepository(Domain).findOneBy({ id: inventory.domainId })
1047
+ }
1048
+
1049
+ @FieldResolver(type => User)
1050
+ async updater(@Root() inventory: Inventory): Promise<User> {
1051
+ return await getRepository(User).findOneBy({ id: inventory.updaterId })
1052
+ }
1053
+
1054
+ @FieldResolver(type => User)
1055
+ async creator(@Root() inventory: Inventory): Promise<User> {
1056
+ return await getRepository(User).findOneBy({ id: inventory.creatorId })
1057
+ }
1058
+
1059
+ /*
1060
+ move changeCount to @FieldResolver so that system don't have
1061
+ to keep searching for this when user does not request for it
1062
+ */
1063
+ @FieldResolver(type => Number)
1064
+ async changeCount(@Root() inventory: Inventory): Promise<Number> {
1065
+ return await getRepository(InventoryChange).count({
1066
+ where: { inventory: { id: inventory.id } }
1067
+ })
1068
+ }
1069
+
1070
+ /*
1071
+ "purchaseOrderNo", "orderProductRemark" and "arrivalNoticeRefNo"
1072
+ are purely for Getha's lot label. @farishelmi added these here to
1073
+ avoid logic changes in @inventories query
1074
+ */
1075
+ @FieldResolver(type => String)
1076
+ async purchaseOrderNo(@Root() inventory: Inventory): Promise<String> {
1077
+ if (!inventory.refOrderId) return ''
1078
+
1079
+ let items: any = await getRepository(Inventory).query(`
1080
+ SELECT po.name as "purchaseOrderNo" FROM arrival_notices an
1081
+ LEFT JOIN purchase_orders po
1082
+ ON an.purchase_order_id = po.id
1083
+ WHERE an.id = '${inventory.refOrderId}'
1084
+ LIMIT 1
1085
+ `)
1086
+
1087
+ return items[0]?.purchaseOrderNo || ''
1088
+ }
1089
+
1090
+ @FieldResolver(type => String)
1091
+ async orderProductRemark(@Root() inventory: Inventory): Promise<String> {
1092
+ if (!inventory.orderProductId) return ''
1093
+
1094
+ let items: any = await getRepository(Inventory).query(`
1095
+ SELECT op.remark as "orderProductRemark" FROM order_products op
1096
+ WHERE op.id = '${inventory.orderProductId}'
1097
+ LIMIT 1
1098
+ `)
1099
+
1100
+ return items[0]?.orderProductRemark || ''
1101
+ }
1102
+
1103
+ @FieldResolver(type => String)
1104
+ async arrivalNoticeRefNo(@Root() inventory: Inventory): Promise<String> {
1105
+ if (!inventory.refOrderId) return ''
1106
+
1107
+ let items: any = await getRepository(Inventory).query(`
1108
+ SELECT an.ref_no as "arrivalNoticeRefNo" FROM arrival_notices an
1109
+ WHERE an.id = '${inventory.refOrderId}'
1110
+ LIMIT 1
1111
+ `)
1112
+
1113
+ return items[0]?.arrivalNoticeRefNo || ''
1114
+ }
1115
+
1116
+ @FieldResolver(type => String)
1117
+ async arrivalNoticeNo(@Root() inventory: Inventory): Promise<String> {
1118
+ if (!inventory.refOrderId) return ''
1119
+
1120
+ let items: any = await getRepository(Inventory).query(`
1121
+ SELECT an.name as "arrivalNoticeNo" FROM arrival_notices an
1122
+ WHERE an.id = '${inventory.refOrderId}'
1123
+ LIMIT 1
1124
+ `)
1125
+
1126
+ return items[0]?.arrivalNoticeNo || ''
1127
+ }
1128
+ }
1129
+
1130
+ export async function inventoriesByStrategy(
1131
+ {
1132
+ worksheetId,
1133
+ batchId,
1134
+ bizplaceId,
1135
+ productName,
1136
+ productSku,
1137
+ packingType,
1138
+ packingSize,
1139
+ uom,
1140
+ pickingStrategy,
1141
+ locationSortingRules
1142
+ },
1143
+ trxMgr: EntityManager
1144
+ ) {
1145
+ const qb = await trxMgr.getRepository(Inventory).createQueryBuilder('INV')
1146
+ qb.innerJoinAndSelect('INV.product', 'PROD')
1147
+ .innerJoinAndSelect('INV.location', 'LOC')
1148
+ .addSelect(subQuery =>
1149
+ subQuery
1150
+ .select('COALESCE(SUM(release_qty), 0)', 'releaseQty')
1151
+ .from('order_inventories', 'OI')
1152
+ .where('"OI"."inventory_id" = "INV"."id"')
1153
+ .andWhere("\"OI\".\"status\" IN ('PENDING', 'PENDING_RECEIVE', 'PENDING_WORKSHEET', 'PENDING_SPLIT')")
1154
+ .andWhere('"OI"."ref_worksheet_id" <> :worksheetId', { worksheetId: worksheetId })
1155
+ )
1156
+ .addSelect(subQuery =>
1157
+ subQuery
1158
+ .select('COALESCE(SUM(release_uom_value), 0)', 'releaseUomValue')
1159
+ .from('order_inventories', 'OI')
1160
+ .where('"OI"."inventory_id" = "INV"."id"')
1161
+ .andWhere("\"OI\".\"status\" IN ('PENDING', 'PENDING_RECEIVE', 'PENDING_WORKSHEET', 'PENDING_SPLIT')")
1162
+ .andWhere('"OI"."ref_worksheet_id" <> :worksheetId', { worksheetId: worksheetId })
1163
+ )
1164
+ .andWhere('"PROD"."name" = :productName')
1165
+ .andWhere('"PROD"."sku" = :productSku')
1166
+ .andWhere('"INV"."packing_type" = :packingType')
1167
+ .andWhere('"INV"."packing_size" = :packingSize')
1168
+ .andWhere('"INV"."uom" = :uom')
1169
+ .andWhere('"INV"."status" = :status', { status: 'STORED' })
1170
+ .andWhere('"LOC"."type" NOT IN (:...locationTypes)', {
1171
+ locationTypes: [LOCATION_TYPE.QUARANTINE, LOCATION_TYPE.RESERVE]
1172
+ })
1173
+ .setParameters({
1174
+ productName,
1175
+ productSku,
1176
+ packingType,
1177
+ packingSize,
1178
+ uom
1179
+ })
1180
+
1181
+ if (batchId !== '') {
1182
+ qb.andWhere('"INV"."batch_id" = :batchId', { batchId })
1183
+ }
1184
+
1185
+ if (bizplaceId) {
1186
+ qb.andWhere('"INV"."bizplace_id" = :bizplaceId', { bizplaceId: bizplaceId })
1187
+ }
1188
+
1189
+ switch (pickingStrategy.toUpperCase()) {
1190
+ case 'FIFO':
1191
+ qb.orderBy('"INV"."created_at"', 'ASC')
1192
+ if (locationSortingRules?.length > 0) {
1193
+ locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
1194
+ qb.addOrderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1195
+ })
1196
+ }
1197
+ break
1198
+
1199
+ case 'LIFO':
1200
+ qb.orderBy('"INV"."created_at"', 'DESC')
1201
+ if (locationSortingRules?.length > 0) {
1202
+ locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
1203
+ qb.addOrderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1204
+ })
1205
+ }
1206
+ break
1207
+
1208
+ case 'FEFO':
1209
+ qb.orderBy('"INV"."expiration_date"', 'ASC')
1210
+ qb.addOrderBy('"INV"."created_at"', 'ASC')
1211
+ if (locationSortingRules?.length > 0) {
1212
+ locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
1213
+ qb.addOrderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1214
+ })
1215
+ }
1216
+ break
1217
+
1218
+ case 'FMFO':
1219
+ qb.orderBy('"INV"."manufacture_date"', 'ASC')
1220
+ qb.addOrderBy('"INV"."created_at"', 'ASC')
1221
+ if (locationSortingRules?.length > 0) {
1222
+ locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
1223
+ qb.addOrderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1224
+ })
1225
+ }
1226
+ break
1227
+
1228
+ case 'LOCATION':
1229
+ if (locationSortingRules?.length > 0) {
1230
+ locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
1231
+ idx === 0
1232
+ ? qb.orderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1233
+ : qb.addOrderBy(`LOC.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
1234
+ })
1235
+ } else qb.orderBy('"LOC"."name"', 'DESC')
1236
+ break
1237
+ }
1238
+
1239
+ const { entities, raw } = await qb.getRawAndEntities()
1240
+ const items = entities
1241
+ .map((inv: Inventory, idx: number) => {
1242
+ const qty: number = inv?.qty > 0 ? inv.qty : 0
1243
+ const uomValue: number = inv?.uomValue > 0 ? inv.uomValue : 0
1244
+ const lockedQty: number = inv.lockedQty || 0
1245
+ const lockedUomValue: number = inv.lockedUomValue || 0
1246
+ const releaseQty: number = parseInt(raw[idx].releaseQty) || 0
1247
+ const releaseUomValue: number = parseFloat(raw[idx].releaseUomValue) || 0
1248
+
1249
+ return {
1250
+ ...inv,
1251
+ qty: qty - lockedQty - releaseQty,
1252
+ uomValue: uomValue - lockedUomValue - releaseUomValue
1253
+ }
1254
+ })
1255
+ .filter((inv: Inventory) => inv.qty)
1256
+
1257
+ const total: number = await qb.getCount()
1258
+ return { items, total }
1259
+ }