@things-factory/worksheet-base 4.3.769 → 4.3.771

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 (52) hide show
  1. package/dist-server/constants/template.js +2 -1
  2. package/dist-server/constants/template.js.map +1 -1
  3. package/dist-server/controllers/inbound/unloading-worksheet-controller.js +17 -0
  4. package/dist-server/controllers/inbound/unloading-worksheet-controller.js.map +1 -1
  5. package/dist-server/controllers/index.js +1 -0
  6. package/dist-server/controllers/index.js.map +1 -1
  7. package/dist-server/controllers/outbound/loading-worksheet-controller.js +53 -0
  8. package/dist-server/controllers/outbound/loading-worksheet-controller.js.map +1 -1
  9. package/dist-server/controllers/outbound/picking-worksheet-controller.js +151 -42
  10. package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
  11. package/dist-server/controllers/render-packing-label.js +160 -0
  12. package/dist-server/controllers/render-packing-label.js.map +1 -0
  13. package/dist-server/graphql/resolvers/worksheet/loading/create-group-loading-packages.js +20 -0
  14. package/dist-server/graphql/resolvers/worksheet/loading/create-group-loading-packages.js.map +1 -0
  15. package/dist-server/graphql/resolvers/worksheet/loading/index.js +2 -1
  16. package/dist-server/graphql/resolvers/worksheet/loading/index.js.map +1 -1
  17. package/dist-server/graphql/resolvers/worksheet/picking-worksheet.js +6 -1
  18. package/dist-server/graphql/resolvers/worksheet/picking-worksheet.js.map +1 -1
  19. package/dist-server/graphql/resolvers/worksheet/putaway-replenishment-worksheet.js +2 -1
  20. package/dist-server/graphql/resolvers/worksheet/putaway-replenishment-worksheet.js.map +1 -1
  21. package/dist-server/graphql/resolvers/worksheet/putaway-returning-worksheet.js +2 -1
  22. package/dist-server/graphql/resolvers/worksheet/putaway-returning-worksheet.js.map +1 -1
  23. package/dist-server/graphql/resolvers/worksheet/putaway-worksheet.js +1 -0
  24. package/dist-server/graphql/resolvers/worksheet/putaway-worksheet.js.map +1 -1
  25. package/dist-server/graphql/resolvers/worksheet/return-worksheet.js +2 -1
  26. package/dist-server/graphql/resolvers/worksheet/return-worksheet.js.map +1 -1
  27. package/dist-server/graphql/types/worksheet/group-loading-package-item.js +11 -0
  28. package/dist-server/graphql/types/worksheet/group-loading-package-item.js.map +1 -0
  29. package/dist-server/graphql/types/worksheet/index.js +8 -1
  30. package/dist-server/graphql/types/worksheet/index.js.map +1 -1
  31. package/dist-server/graphql/types/worksheet/worksheet-detail-info.js +1 -0
  32. package/dist-server/graphql/types/worksheet/worksheet-detail-info.js.map +1 -1
  33. package/dist-server/routes.js +11 -0
  34. package/dist-server/routes.js.map +1 -1
  35. package/package.json +13 -13
  36. package/server/constants/template.ts +2 -1
  37. package/server/controllers/inbound/unloading-worksheet-controller.ts +20 -0
  38. package/server/controllers/index.ts +1 -0
  39. package/server/controllers/outbound/loading-worksheet-controller.ts +67 -0
  40. package/server/controllers/outbound/picking-worksheet-controller.ts +167 -43
  41. package/server/controllers/render-packing-label.ts +199 -0
  42. package/server/graphql/resolvers/worksheet/loading/create-group-loading-packages.ts +32 -0
  43. package/server/graphql/resolvers/worksheet/loading/index.ts +3 -1
  44. package/server/graphql/resolvers/worksheet/picking-worksheet.ts +5 -1
  45. package/server/graphql/resolvers/worksheet/putaway-replenishment-worksheet.ts +2 -1
  46. package/server/graphql/resolvers/worksheet/putaway-returning-worksheet.ts +2 -1
  47. package/server/graphql/resolvers/worksheet/putaway-worksheet.ts +1 -0
  48. package/server/graphql/resolvers/worksheet/return-worksheet.ts +2 -1
  49. package/server/graphql/types/worksheet/group-loading-package-item.ts +8 -0
  50. package/server/graphql/types/worksheet/index.ts +8 -1
  51. package/server/graphql/types/worksheet/worksheet-detail-info.ts +1 -0
  52. package/server/routes.ts +11 -0
@@ -164,6 +164,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
164
164
  expirationDate: inventory?.expirationDate ? new Date(inventory.expirationDate) : undefined,
165
165
  status: INVENTORY_STATUS.UNLOADED,
166
166
  qty: qty,
167
+ conditionOfGoods: inventory?.conditionOfGoods,
167
168
  manufactureDate: inventory?.manufactureDate ? new Date(inventory.manufactureDate) : undefined,
168
169
  }
169
170
 
@@ -357,6 +358,12 @@ export class UnloadingWorksheetController extends VasWorksheetController {
357
358
  })
358
359
  }
359
360
 
361
+ if (inventory?.conditionOfGoods) {
362
+ invQb.andWhere('INV.condition_of_goods = :conditionOfGoods', {
363
+ conditionOfGoods: inventory.conditionOfGoods
364
+ })
365
+ }
366
+
360
367
  if (inventory?.cartonId) invQb.andWhere('INV.carton_id = :cartonId', { cartonId: inventory.cartonId })
361
368
  else invQb.andWhere('INV.pallet_id = :palletId', { palletId: inventory.palletId })
362
369
 
@@ -375,6 +382,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
375
382
  targetProduct.packQty
376
383
  : 0
377
384
  : null
385
+ const conditionOfGoods: string = inventory?.conditionOfGoods
378
386
 
379
387
  if (!foundInventory) {
380
388
  let newInventory: Partial<Inventory> = new Inventory()
@@ -413,6 +421,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
413
421
  newInventory.location = location
414
422
  newInventory.zone = zone
415
423
  newInventory.status = INVENTORY_STATUS.CHECKED
424
+ newInventory.conditionOfGoods = conditionOfGoods
416
425
  newInventory.creator = this.user
417
426
 
418
427
  if (inventory?.expirationDate) {
@@ -451,6 +460,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
451
460
  : targetInventory.returnUomValue / targetInventory.returnQty) *
452
461
  1000
453
462
  ) / 1000
463
+ foundInventory.conditionOfGoods = conditionOfGoods ?? foundInventory.conditionOfGoods // DEFAULT BACK TO PREVIOUS INVENTORY'S CONDITION OF GOODS
454
464
 
455
465
  //refer to scanUnload
456
466
  foundInventory = await this.transactionInventory(
@@ -593,6 +603,12 @@ export class UnloadingWorksheetController extends VasWorksheetController {
593
603
  })
594
604
  }
595
605
 
606
+ if (inventory?.conditionOfGoods) {
607
+ invQb.andWhere('INV.condition_of_goods = :conditionOfGoods', {
608
+ conditionOfGoods: inventory.conditionOfGoods
609
+ })
610
+ }
611
+
596
612
  if (inventory?.cartonId) invQb.andWhere('INV.carton_id = :cartonId', { cartonId: inventory.cartonId })
597
613
  else invQb.andWhere('INV.pallet_id = :palletId', { palletId: inventory.palletId })
598
614
 
@@ -611,6 +627,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
611
627
  targetProduct.packQty
612
628
  : 0
613
629
  : null
630
+ const conditionOfGoods: string = inventory?.conditionOfGoods
614
631
 
615
632
  if (!foundInventory) {
616
633
  let newInventory: Partial<Inventory> = new Inventory()
@@ -649,6 +666,7 @@ export class UnloadingWorksheetController extends VasWorksheetController {
649
666
  newInventory.location = location
650
667
  newInventory.zone = zone
651
668
  newInventory.status = INVENTORY_STATUS.CHECKED
669
+ newInventory.conditionOfGoods = conditionOfGoods
652
670
  newInventory.creator = this.user
653
671
 
654
672
  if (inventory?.expirationDate) {
@@ -679,6 +697,8 @@ export class UnloadingWorksheetController extends VasWorksheetController {
679
697
  1000
680
698
  ) / 1000
681
699
  foundInventory.productDetail = productDetail
700
+ foundInventory.conditionOfGoods = conditionOfGoods ?? foundInventory.conditionOfGoods // DEFAULT BACK TO PREVIOUS INVENTORY'S CONDITION OF GOODS
701
+
682
702
  if (arrivalNotice) targetProduct.actualPackQty = targetProduct.actualPackQty + qty
683
703
  else if (returnOrder) targetInventory.actualPackQty = targetInventory.actualPackQty + qty
684
704
 
@@ -1,6 +1,7 @@
1
1
  export * from './render-ro-do'
2
2
  export * from './render-grn'
3
3
  export * from './render-manual-do'
4
+ export * from './render-packing-label'
4
5
  export * from './render-elccl-grn'
5
6
  export * from './render-job-sheet'
6
7
  export * from './render-kimeda-grn'
@@ -1,13 +1,17 @@
1
1
  import { Equal, In, Not, IsNull } from 'typeorm'
2
+ import { v4 as uuidv4 } from 'uuid'
2
3
 
3
4
  import { Bizplace } from '@things-factory/biz-base'
4
5
  import {
5
6
  DeliveryOrder,
7
+ LoadingPackages,
8
+ LoadingPackageItems,
6
9
  ORDER_INVENTORY_STATUS,
7
10
  ORDER_STATUS,
8
11
  ORDER_TYPES,
9
12
  OrderInventory,
10
13
  OrderNoGenerator,
14
+ OrderProduct,
11
15
  OrderTote,
12
16
  OrderToteItem,
13
17
  OrderVas,
@@ -443,4 +447,67 @@ export class LoadingWorksheetController extends VasWorksheetController {
443
447
 
444
448
  return completeWorksheet
445
449
  }
450
+
451
+ async createGroupLoadingPackages(
452
+ releaseGood: ReleaseGood,
453
+ groupedItems: Array<{ orderProductId: string; groupQty: number }>
454
+ ): Promise<LoadingPackages> {
455
+ // Create LoadingPackages
456
+ const loadingPackageName = OrderNoGenerator.loadingPackage()
457
+ const loadingPackage: LoadingPackages = await this.trxMgr.getRepository(LoadingPackages).save({
458
+ name: loadingPackageName,
459
+ domain: this.domain,
460
+ bizplace: releaseGood.bizplace,
461
+ releaseGood: releaseGood,
462
+ status: ORDER_STATUS.DONE,
463
+ creator: this.user,
464
+ updater: this.user
465
+ })
466
+
467
+ // Create LoadingPackageItems for each grouped item
468
+ const loadingPackageItems: LoadingPackageItems[] = []
469
+
470
+ for (const groupedItem of groupedItems) {
471
+ // Validate groupQty
472
+ if (groupedItem.groupQty == null || isNaN(groupedItem.groupQty)) {
473
+ throw new Error('Invalid group quantity')
474
+ }
475
+ if (groupedItem.groupQty <= 0) {
476
+ throw new Error('Group quantity must be greater than zero')
477
+ }
478
+
479
+ // Fetch the OrderProduct directly
480
+ const orderProduct: OrderProduct | null = await this.trxMgr.getRepository(OrderProduct).findOne({
481
+ where: { id: groupedItem.orderProductId, domain: this.domain, releaseGood: releaseGood },
482
+ relations: ['productDetail']
483
+ })
484
+
485
+ if (!orderProduct) {
486
+ throw new Error(`Order product not found: ${groupedItem.orderProductId}`)
487
+ }
488
+
489
+ const loadingPackageItemName = OrderNoGenerator.loadingPackageItem()
490
+ loadingPackageItems.push({
491
+ name: loadingPackageItemName,
492
+ domain: this.domain,
493
+ bizplace: releaseGood.bizplace,
494
+ loadingPackage: loadingPackage,
495
+ orderProduct,
496
+ packedQty: groupedItem.groupQty,
497
+ productDetail: orderProduct.productDetail ?? null,
498
+ status: ORDER_STATUS.DONE,
499
+ creator: this.user,
500
+ updater: this.user
501
+ } as LoadingPackageItems)
502
+ }
503
+
504
+ // Save all loading package items
505
+ await this.trxMgr.getRepository(LoadingPackageItems).save(loadingPackageItems)
506
+
507
+ // Reload with relations
508
+ return await this.trxMgr.getRepository(LoadingPackages).findOne({
509
+ where: { id: loadingPackage.id },
510
+ relations: ['loadingPackageItems', 'loadingPackageItems.orderProduct']
511
+ })
512
+ }
446
513
  }
@@ -795,10 +795,28 @@ export class PickingWorksheetController extends VasWorksheetController {
795
795
  newTargetInventory.updater = this.user
796
796
  newTargetInventory = await this.trxMgr.getRepository(OrderInventory).save(newTargetInventory)
797
797
 
798
- // Update locked qty and uomValue of inventory
799
- inventory.lockedQty = targetInventory.releaseQty + (inventory.lockedQty || 0)
800
- inventory.lockedUomValue = targetInventory.releaseUomValue + (inventory.lockedUomValue || 0)
801
- await this.updateInventory(inventory)
798
+ // Atomic update of locked qty and uomValue of inventory
799
+ const lockResult = await this.trxMgr
800
+ .getRepository(Inventory)
801
+ .createQueryBuilder()
802
+ .update(Inventory)
803
+ .set({
804
+ lockedQty: () => `COALESCE("locked_qty", 0) + :releaseQty::numeric`,
805
+ lockedUomValue: () => `COALESCE("locked_uom_value", 0) + :releaseUomValue::numeric`,
806
+ updater: this.user,
807
+ updatedAt: new Date()
808
+ })
809
+ .setParameter('releaseQty', targetInventory.releaseQty)
810
+ .setParameter('releaseUomValue', targetInventory.releaseUomValue)
811
+ .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
812
+ id: inventory.id,
813
+ newQty: targetInventory.releaseQty
814
+ })
815
+ .execute()
816
+
817
+ if (lockResult.affected === 0) {
818
+ throw new Error(`Insufficient inventory for picking assignment`)
819
+ }
802
820
 
803
821
  // Create worksheet details
804
822
  await this.createWorksheetDetails(worksheet, WORKSHEET_TYPE.PICKING, [newTargetInventory])
@@ -837,12 +855,21 @@ export class PickingWorksheetController extends VasWorksheetController {
837
855
  const targetInventory: OrderInventory = worksheetDetail.targetInventory
838
856
  targetInventoryIds.push(targetInventory.id)
839
857
 
840
- let inventory: Inventory = await this.trxMgr
858
+ // Atomic update of locked qty and uomValue of inventory
859
+ await this.trxMgr
841
860
  .getRepository(Inventory)
842
- .findOne({ where: { id: worksheetDetail.targetInventory.inventory.id } })
843
- inventory.lockedQty = inventory.lockedQty - targetInventory.releaseQty
844
- inventory.lockedUomValue = inventory.lockedUomValue - targetInventory.releaseUomValue
845
- await this.updateInventory(inventory)
861
+ .createQueryBuilder()
862
+ .update(Inventory)
863
+ .set({
864
+ lockedQty: () => `GREATEST(COALESCE("locked_qty", 0) - :releaseQty::numeric, 0)`,
865
+ lockedUomValue: () => `GREATEST(COALESCE("locked_uom_value", 0) - :releaseUomValue::numeric, 0)`,
866
+ updater: this.user,
867
+ updatedAt: new Date()
868
+ })
869
+ .setParameter('releaseQty', targetInventory.releaseQty)
870
+ .setParameter('releaseUomValue', targetInventory.releaseUomValue)
871
+ .where('id = :id', { id: worksheetDetail.targetInventory.inventory.id })
872
+ .execute()
846
873
 
847
874
  await this.trxMgr
848
875
  .getRepository(OrderProduct)
@@ -1008,6 +1035,14 @@ export class PickingWorksheetController extends VasWorksheetController {
1008
1035
  pickedUomValue = matchingProduct.uomValue
1009
1036
  }
1010
1037
 
1038
+ // validation to prevent decimal quantities for non-decimal products
1039
+ const scanPickProduct: Product = await this.trxMgr.getRepository(Product).findOne({
1040
+ where: { id: worksheetDetailInfos.productId }
1041
+ })
1042
+ if (pickedQty % 1 !== 0 && !scanPickProduct?.isInventoryDecimal) {
1043
+ throw new Error('Decimal quantities are not allowed for this product')
1044
+ }
1045
+
1011
1046
  // //validation to prevent over release
1012
1047
  if (!targetInventory)
1013
1048
  throw new Error(this.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `inventory not assigned`))
@@ -1088,14 +1123,14 @@ export class PickingWorksheetController extends VasWorksheetController {
1088
1123
  targetInventory.pickedQty = (targetInventory?.pickedQty || 0) + pickedQty
1089
1124
 
1090
1125
  let updateOiObj = {
1091
- pickedQty: () => `"picked_qty" + ${pickedQty}`,
1126
+ pickedQty: () => `"picked_qty" + :pickedQty`,
1092
1127
  updatedAt: new Date(),
1093
1128
  updater: this.user,
1094
1129
  pickedBy: this.user.name,
1095
1130
  pickedByUser: this.user,
1096
1131
  pickedAt: new Date(),
1097
1132
  status: () =>
1098
- `case when release_qty = "picked_qty" + ${pickedQty} then '${ORDER_INVENTORY_STATUS.PICKED}' else status end`
1133
+ `case when release_qty = "picked_qty" + :pickedQty then '${ORDER_INVENTORY_STATUS.PICKED}' else status end`
1099
1134
  }
1100
1135
 
1101
1136
  if (targetInventory.binLocation) {
@@ -1107,6 +1142,7 @@ export class PickingWorksheetController extends VasWorksheetController {
1107
1142
  .createQueryBuilder()
1108
1143
  .update(OrderInventory)
1109
1144
  .set(updateOiObj)
1145
+ .setParameter('pickedQty', pickedQty)
1110
1146
  .where({ id: targetInventory.id })
1111
1147
  .andWhere(`picked_qty + :pickedQty <= release_qty`, { pickedQty })
1112
1148
  .execute()
@@ -1130,12 +1166,12 @@ export class PickingWorksheetController extends VasWorksheetController {
1130
1166
  let releaseUomValue = Math.trunc((pickedUomValue / pickedQty) * releaseQty * 1000) / 1000
1131
1167
 
1132
1168
  let updateInvObj = {
1133
- qty: () => `"qty" - ${releaseQty}`,
1134
- lockedQty: () => `"locked_qty" - ${releaseQty}`,
1135
- uomValue: () => `"uom_value" - ${releaseUomValue}`,
1136
- lockedUomValue: () => `"locked_uom_value" - ${releaseUomValue}`,
1169
+ qty: () => `"qty" - :deductQty::numeric`,
1170
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
1171
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
1172
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
1137
1173
  status: () =>
1138
- `case when "qty" - ${releaseQty} <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1174
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1139
1175
  updater: this.user,
1140
1176
  updatedAt: new Date()
1141
1177
  }
@@ -1145,6 +1181,8 @@ export class PickingWorksheetController extends VasWorksheetController {
1145
1181
  .createQueryBuilder()
1146
1182
  .update(Inventory)
1147
1183
  .set(updateInvObj)
1184
+ .setParameter('deductQty', releaseQty)
1185
+ .setParameter('deductUomValue', releaseUomValue)
1148
1186
  .where('id = :id', { id: worksheetDetailInfos.inventoryId })
1149
1187
  .returning(['qty'])
1150
1188
  .execute()
@@ -1254,6 +1292,11 @@ export class PickingWorksheetController extends VasWorksheetController {
1254
1292
  })
1255
1293
  if (!oiValidate) throw new Error(this.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `is done`))
1256
1294
 
1295
+ // validation to prevent decimal quantities for non-decimal products
1296
+ if (pickedQty % 1 !== 0 && !product.isInventoryDecimal) {
1297
+ throw new Error('Decimal quantities are not allowed for this product')
1298
+ }
1299
+
1257
1300
  //validation to prevent over release
1258
1301
  if (inventory.qty <= 0) throw new Error(this.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `over release`))
1259
1302
 
@@ -1426,6 +1469,16 @@ export class PickingWorksheetController extends VasWorksheetController {
1426
1469
  if (sumOfReleaseQty != releaseQty)
1427
1470
  throw new Error(this.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `insufficient picking quantity`))
1428
1471
 
1472
+ // validation to prevent decimal quantities for non-decimal products
1473
+ if (targetInventories.length > 0) {
1474
+ const batchPickProduct: Product = await this.trxMgr.getRepository(Product).findOne({
1475
+ where: { id: targetInventories[0].productId }
1476
+ })
1477
+ if (releaseQty % 1 !== 0 && !batchPickProduct?.isInventoryDecimal) {
1478
+ throw new Error('Decimal quantities are not allowed for this product')
1479
+ }
1480
+ }
1481
+
1429
1482
  for (var i = 0; i < targetInventories.length; i++) {
1430
1483
  let targetInventory: OrderInventory = targetInventories[i]
1431
1484
  let inventory: Inventory = await this.trxMgr.getRepository(Inventory).findOne({
@@ -1480,10 +1533,12 @@ export class PickingWorksheetController extends VasWorksheetController {
1480
1533
  await this.updateOrderTargets([targetInventory])
1481
1534
 
1482
1535
  let updateInvObj = {
1483
- qty: () => `"qty" - ${targetInventory.releaseQty}`,
1484
- lockedQty: () => `"locked_qty" - ${targetInventory.releaseQty}`,
1485
- uomValue: () => `"uom_value" - ${targetInventory.releaseUomValue}`,
1486
- lockedUomValue: () => `"locked_uom_value" - ${targetInventory.releaseUomValue}`,
1536
+ qty: () => `"qty" - :deductQty::numeric`,
1537
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
1538
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
1539
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
1540
+ status: () =>
1541
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1487
1542
  updater: this.user,
1488
1543
  updatedAt: new Date()
1489
1544
  }
@@ -1493,6 +1548,8 @@ export class PickingWorksheetController extends VasWorksheetController {
1493
1548
  .createQueryBuilder()
1494
1549
  .update(Inventory)
1495
1550
  .set(updateInvObj)
1551
+ .setParameter('deductQty', targetInventory.releaseQty)
1552
+ .setParameter('deductUomValue', targetInventory.releaseUomValue)
1496
1553
  .where('id = :id', { id: targetInventory.inventory.id })
1497
1554
  .returning(['qty'])
1498
1555
  .execute()
@@ -1599,6 +1656,11 @@ export class PickingWorksheetController extends VasWorksheetController {
1599
1656
 
1600
1657
  pickedQty = matchingProduct.qty
1601
1658
 
1659
+ // validation to prevent decimal quantities for non-decimal products
1660
+ if (pickedQty % 1 !== 0 && !product?.isInventoryDecimal) {
1661
+ throw new Error('Decimal quantities are not allowed for this product')
1662
+ }
1663
+
1602
1664
  const sumOfReleaseQty: number = parseFloat(
1603
1665
  targetInventories
1604
1666
  .map((oi: OrderInventory) => oi.releaseQty)
@@ -1678,10 +1740,12 @@ export class PickingWorksheetController extends VasWorksheetController {
1678
1740
  await this.updateOrderTargets([targetInventory])
1679
1741
 
1680
1742
  let updateInvObj = {
1681
- qty: () => `"qty" - ${targetInventory.releaseQty}`,
1682
- lockedQty: () => `"locked_qty" - ${targetInventory.releaseQty}`,
1683
- uomValue: () => `"uom_value" - ${targetInventory.releaseUomValue}`,
1684
- lockedUomValue: () => `"locked_uom_value" - ${targetInventory.releaseUomValue}`,
1743
+ qty: () => `"qty" - :deductQty::numeric`,
1744
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
1745
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
1746
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
1747
+ status: () =>
1748
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1685
1749
  updater: this.user,
1686
1750
  updatedAt: new Date()
1687
1751
  }
@@ -1691,6 +1755,8 @@ export class PickingWorksheetController extends VasWorksheetController {
1691
1755
  .createQueryBuilder()
1692
1756
  .update(Inventory)
1693
1757
  .set(updateInvObj)
1758
+ .setParameter('deductQty', targetInventory.releaseQty)
1759
+ .setParameter('deductUomValue', targetInventory.releaseUomValue)
1694
1760
  .where('id = :id', { id: targetInventory.inventory.id })
1695
1761
  .returning(['qty'])
1696
1762
  .execute()
@@ -1966,6 +2032,14 @@ export class PickingWorksheetController extends VasWorksheetController {
1966
2032
  }
1967
2033
 
1968
2034
  private async updatePickingTransaction(releaseGood, orderInventory, worksheetDetail, inventory, pickedQty) {
2035
+ // validation to prevent decimal quantities for non-decimal products
2036
+ const pickTxProduct: Product = await this.trxMgr.getRepository(Product).findOne({
2037
+ where: { id: orderInventory.productId }
2038
+ })
2039
+ if (pickedQty % 1 !== 0 && !pickTxProduct?.isInventoryDecimal) {
2040
+ throw new Error('Decimal quantities are not allowed for this product')
2041
+ }
2042
+
1969
2043
  const releaseQty: number = orderInventory.releaseQty
1970
2044
 
1971
2045
  orderInventory.pickedQty = (orderInventory?.pickedQty || 0) + pickedQty
@@ -1980,18 +2054,40 @@ export class PickingWorksheetController extends VasWorksheetController {
1980
2054
  orderInventory.pickedByUser = this.user
1981
2055
  orderInventory.pickedAt = new Date()
1982
2056
 
1983
- inventory.qty -= orderInventory.releaseQty
1984
- inventory.uomValue = Math.round((inventory.uomValue - orderInventory.releaseUomValue) * 100) / 100
1985
- inventory.lockedQty = inventory.lockedQty - orderInventory.releaseQty
1986
- inventory.lockedUomValue = Math.round((inventory.lockedUomValue - orderInventory.releaseUomValue) * 100) / 100
1987
- inventory = await this.transactionInventory(
2057
+ // Atomic SQL update instead of stale-read pattern
2058
+ await this.trxMgr
2059
+ .getRepository(Inventory)
2060
+ .createQueryBuilder()
2061
+ .update(Inventory)
2062
+ .set({
2063
+ qty: () => `"qty" - :deductQty::numeric`,
2064
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
2065
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
2066
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
2067
+ status: () =>
2068
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
2069
+ updater: this.user,
2070
+ updatedAt: new Date()
2071
+ })
2072
+ .setParameter('deductQty', orderInventory.releaseQty)
2073
+ .setParameter('deductUomValue', orderInventory.releaseUomValue)
2074
+ .where('id = :id', { id: inventory.id })
2075
+ .execute()
2076
+
2077
+ // Generate inventory history separately
2078
+ await generateInventoryHistory(
1988
2079
  inventory,
1989
2080
  releaseGood,
2081
+ INVENTORY_TRANSACTION_TYPE.PICKING,
1990
2082
  -orderInventory.releaseQty,
1991
2083
  -orderInventory.releaseUomValue,
1992
- INVENTORY_TRANSACTION_TYPE.PICKING
2084
+ this.user,
2085
+ this.trxMgr
1993
2086
  )
1994
2087
 
2088
+ // Re-read inventory for downstream use (status check for TERMINATED)
2089
+ inventory = await this.trxMgr.getRepository(Inventory).findOne({ where: { id: inventory.id } })
2090
+
1995
2091
  worksheetDetail.status = WORKSHEET_STATUS.DONE
1996
2092
  worksheetDetail.updater = this.user
1997
2093
  await this.trxMgr.getRepository(WorksheetDetail).save(worksheetDetail)
@@ -2545,20 +2641,37 @@ export class PickingWorksheetController extends VasWorksheetController {
2545
2641
  // update inventory locked qty and uom value
2546
2642
  oi = await transaction.getRepository(OrderInventory).save({ ...oi })
2547
2643
 
2548
- await transaction.getRepository(Inventory).update(oi.inventory.id, {
2549
- lockedQty: (oi.inventory?.lockedQty || 0) + oi.releaseQty,
2550
- lockedUomValue: (oi.inventory?.lockedUomValue || 0) + oi.releaseUomValue,
2551
- updater: this.user
2552
- })
2644
+ const lockResult = await transaction
2645
+ .getRepository(Inventory)
2646
+ .createQueryBuilder()
2647
+ .update(Inventory)
2648
+ .set({
2649
+ lockedQty: () => `COALESCE("locked_qty", 0) + :releaseQty::numeric`,
2650
+ lockedUomValue: () => `COALESCE("locked_uom_value", 0) + :releaseUomValue::numeric`,
2651
+ updater: this.user
2652
+ })
2653
+ .setParameter('releaseQty', oi.releaseQty)
2654
+ .setParameter('releaseUomValue', oi.releaseUomValue)
2655
+ .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
2656
+ id: oi.inventory.id,
2657
+ newQty: oi.releaseQty
2658
+ })
2659
+ .execute()
2660
+
2661
+ if (lockResult.affected === 0) {
2662
+ throw new Error(`Insufficient inventory for picking assignment`)
2663
+ }
2553
2664
 
2554
2665
  await transaction
2555
2666
  .getRepository(ProductDetailStock)
2556
2667
  .createQueryBuilder()
2557
2668
  .update(ProductDetailStock)
2558
2669
  .set({
2559
- unassignedQty: () => `"unassigned_qty" - ${oi.releaseQty}`,
2560
- unassignedUomValue: () => `"unassigned_uom_value" - ${oi.releaseUomValue}`
2670
+ unassignedQty: () => `GREATEST("unassigned_qty" - :oiReleaseQty::numeric, 0)`,
2671
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :oiReleaseUomValue::numeric, 0)`
2561
2672
  })
2673
+ .setParameter('oiReleaseQty', oi.releaseQty)
2674
+ .setParameter('oiReleaseUomValue', oi.releaseUomValue)
2562
2675
  .where({ productDetail: oi.productDetail.id })
2563
2676
  .execute()
2564
2677
 
@@ -2643,26 +2756,37 @@ export class PickingWorksheetController extends VasWorksheetController {
2643
2756
  releaseUomValue = releaseUomValue - allocatedUomValue
2644
2757
 
2645
2758
  //// Update inventory locked quantity
2646
- await this.trxMgr
2759
+ const lockResult = await this.trxMgr
2647
2760
  .getRepository(Inventory)
2648
2761
  .createQueryBuilder('inv')
2649
2762
  .update(Inventory)
2650
2763
  .set({
2651
- lockedUomValue: () => `COALESCE(locked_uom_value,0) + ${allocatedUomValue}`,
2652
- lockedQty: () => `COALESCE(locked_qty,0) + ${allocatedQty}`
2764
+ lockedUomValue: () => `COALESCE(locked_uom_value,0) + :allocatedUomValue::numeric`,
2765
+ lockedQty: () => `COALESCE(locked_qty,0) + :allocatedQty::numeric`
2766
+ })
2767
+ .setParameter('allocatedUomValue', allocatedUomValue)
2768
+ .setParameter('allocatedQty', allocatedQty)
2769
+ .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
2770
+ id: targetInventory.id,
2771
+ newQty: allocatedQty
2653
2772
  })
2654
- .where('id = :id', { id: targetInventory.id })
2655
2773
  .execute()
2656
2774
 
2775
+ if (lockResult.affected === 0) {
2776
+ throw new Error(`Insufficient inventory for picking assignment`)
2777
+ }
2778
+
2657
2779
  // update product detail stock deduct unassigned qty and unassigned uom value
2658
2780
  await this.trxMgr
2659
2781
  .getRepository(ProductDetailStock)
2660
2782
  .createQueryBuilder()
2661
2783
  .update(ProductDetailStock)
2662
2784
  .set({
2663
- unassignedQty: () => `"unassigned_qty" - ${allocatedQty}`,
2664
- unassignedUomValue: () => `"unassigned_uom_value" - ${allocatedUomValue}`
2785
+ unassignedQty: () => `GREATEST("unassigned_qty" - :deductQty::numeric, 0)`,
2786
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :deductUomValue::numeric, 0)`
2665
2787
  })
2788
+ .setParameter('deductQty', allocatedQty)
2789
+ .setParameter('deductUomValue', allocatedUomValue)
2666
2790
  .where({ productDetail: orderProducts[i].productDetail.id })
2667
2791
  .execute()
2668
2792