@things-factory/worksheet-base 4.3.770 → 4.3.772

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 (28) hide show
  1. package/dist-server/controllers/inbound/putaway-returning-worksheet-controller.js +10 -5
  2. package/dist-server/controllers/inbound/putaway-returning-worksheet-controller.js.map +1 -1
  3. package/dist-server/controllers/inbound/putaway-worksheet-controller.js +10 -5
  4. package/dist-server/controllers/inbound/putaway-worksheet-controller.js.map +1 -1
  5. package/dist-server/controllers/inbound/unloading-worksheet-controller.js +45 -19
  6. package/dist-server/controllers/inbound/unloading-worksheet-controller.js.map +1 -1
  7. package/dist-server/controllers/outbound/packing-worksheet-controller.js +2 -2
  8. package/dist-server/controllers/outbound/packing-worksheet-controller.js.map +1 -1
  9. package/dist-server/controllers/outbound/picking-worksheet-controller.js +169 -44
  10. package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
  11. package/dist-server/controllers/outbound/returning-worksheet-controller.js +36 -3
  12. package/dist-server/controllers/outbound/returning-worksheet-controller.js.map +1 -1
  13. package/dist-server/controllers/outbound/sorting-worksheet-controller.js +2 -2
  14. package/dist-server/controllers/outbound/sorting-worksheet-controller.js.map +1 -1
  15. package/dist-server/controllers/replenishment/replenishment-worksheet-controller.js +1 -1
  16. package/dist-server/controllers/replenishment/replenishment-worksheet-controller.js.map +1 -1
  17. package/dist-server/graphql/resolvers/worksheet/generate-worksheet/generate-release-good-worksheet.js +12 -8
  18. package/dist-server/graphql/resolvers/worksheet/generate-worksheet/generate-release-good-worksheet.js.map +1 -1
  19. package/package.json +4 -4
  20. package/server/controllers/inbound/putaway-returning-worksheet-controller.ts +26 -5
  21. package/server/controllers/inbound/putaway-worksheet-controller.ts +26 -5
  22. package/server/controllers/inbound/unloading-worksheet-controller.ts +66 -23
  23. package/server/controllers/outbound/packing-worksheet-controller.ts +3 -2
  24. package/server/controllers/outbound/picking-worksheet-controller.ts +211 -45
  25. package/server/controllers/outbound/returning-worksheet-controller.ts +41 -16
  26. package/server/controllers/outbound/sorting-worksheet-controller.ts +3 -2
  27. package/server/controllers/replenishment/replenishment-worksheet-controller.ts +1 -7
  28. package/server/graphql/resolvers/worksheet/generate-worksheet/generate-release-good-worksheet.ts +14 -16
@@ -440,19 +440,17 @@ export class UnloadingWorksheetController extends VasWorksheetController {
440
440
  foundInventory = await this.trxMgr.getRepository(Inventory).save(newInventory)
441
441
 
442
442
  //refer to scanUnload
443
- newInventory = await this.transactionInventory(
444
- newInventory,
443
+ await generateInventoryHistory(
444
+ foundInventory,
445
445
  Boolean(arrivalNotice) ? arrivalNotice : returnOrder,
446
- newInventory.qty,
447
- newInventory.uomValue,
448
- INVENTORY_TRANSACTION_TYPE.UNLOADING
446
+ INVENTORY_TRANSACTION_TYPE.UNLOADING,
447
+ foundInventory.qty,
448
+ foundInventory.uomValue,
449
+ this.user,
450
+ this.trxMgr
449
451
  )
450
452
  } else {
451
- const updatedQty: number = foundInventory.qty + qty
452
- foundInventory.expirationDate = foundInventory.expirationDate ? new Date(foundInventory.expirationDate) : null
453
- foundInventory.manufactureDate = foundInventory?.manufactureDate ? new Date(foundInventory?.manufactureDate) : null
454
- foundInventory.qty = updatedQty
455
- foundInventory.uomValue +=
453
+ const addUomValue: number =
456
454
  Math.round(
457
455
  qty *
458
456
  (Boolean(arrivalNotice)
@@ -460,20 +458,39 @@ export class UnloadingWorksheetController extends VasWorksheetController {
460
458
  : targetInventory.returnUomValue / targetInventory.returnQty) *
461
459
  1000
462
460
  ) / 1000
463
- foundInventory.conditionOfGoods = conditionOfGoods ?? foundInventory.conditionOfGoods // DEFAULT BACK TO PREVIOUS INVENTORY'S CONDITION OF GOODS
464
461
 
465
- //refer to scanUnload
466
- foundInventory = await this.transactionInventory(
462
+ const updateFields: any = {
463
+ qty: () => `"qty" + :addQty::numeric`,
464
+ uomValue: () => `"uom_value" + :addUomValue::numeric`,
465
+ updater: this.user,
466
+ updatedAt: new Date()
467
+ }
468
+ if (conditionOfGoods) {
469
+ updateFields.conditionOfGoods = conditionOfGoods
470
+ }
471
+ await this.trxMgr.getRepository(Inventory).createQueryBuilder()
472
+ .update(Inventory).set(updateFields)
473
+ .setParameter('addQty', qty)
474
+ .setParameter('addUomValue', addUomValue)
475
+ .where('id = :id', { id: foundInventory.id }).execute()
476
+
477
+ // Update in-memory for downstream
478
+ foundInventory.qty += qty
479
+ foundInventory.uomValue += addUomValue
480
+ if (conditionOfGoods) foundInventory.conditionOfGoods = conditionOfGoods
481
+
482
+ await generateInventoryHistory(
467
483
  foundInventory,
468
484
  Boolean(arrivalNotice) ? arrivalNotice : returnOrder,
485
+ INVENTORY_TRANSACTION_TYPE.UNLOADING,
469
486
  foundInventory.qty,
470
487
  foundInventory.uomValue,
471
- INVENTORY_TRANSACTION_TYPE.UNLOADING
488
+ this.user,
489
+ this.trxMgr
472
490
  )
473
491
 
474
492
  if (arrivalNotice) targetProduct.actualPackQty = targetProduct.actualPackQty + qty
475
493
  else if (returnOrder) targetInventory.actualPackQty = targetInventory.actualPackQty + qty
476
- foundInventory = await this.trxMgr.getRepository(Inventory).save(foundInventory)
477
494
  }
478
495
 
479
496
  let inventoryItem: InventoryItem = new InventoryItem()
@@ -770,14 +787,20 @@ export class UnloadingWorksheetController extends VasWorksheetController {
770
787
  let foundInventory: Inventory = await invQb.getOne()
771
788
  if (!foundInventory) throw new Error(this.ERROR_MSG.FIND.NO_RESULT(foundInventory.palletId))
772
789
 
790
+ await this.trxMgr.getRepository(Inventory).update(
791
+ { id: foundInventory.id },
792
+ { status: INVENTORY_STATUS.UNLOADED, updater: this.user }
793
+ )
773
794
  foundInventory.status = INVENTORY_STATUS.UNLOADED
774
795
 
775
- foundInventory = await this.transactionInventory(
796
+ await generateInventoryHistory(
776
797
  foundInventory,
777
798
  Boolean(arrivalNotice) ? arrivalNotice : returnOrder,
799
+ INVENTORY_TRANSACTION_TYPE.UNLOADING,
778
800
  foundInventory.qty,
779
801
  foundInventory.uomValue,
780
- INVENTORY_TRANSACTION_TYPE.UNLOADING
802
+ this.user,
803
+ this.trxMgr
781
804
  )
782
805
  }
783
806
 
@@ -888,14 +911,23 @@ export class UnloadingWorksheetController extends VasWorksheetController {
888
911
  await this.trxMgr.getRepository(Inventory).save(inventory)
889
912
 
890
913
  if (inventory.qty == 0) {
914
+ await this.trxMgr.getRepository(Inventory).createQueryBuilder()
915
+ .update(Inventory).set({
916
+ lastSeq: () => `"last_seq" + 1`,
917
+ status: INVENTORY_STATUS.DELETED,
918
+ updater: this.user, updatedAt: new Date()
919
+ })
920
+ .where('id = :id', { id: inventory.id }).execute()
891
921
  inventory.lastSeq++
892
922
  inventory.status = INVENTORY_STATUS.DELETED
893
- inventory = await this.transactionInventory(
923
+ await generateInventoryHistory(
894
924
  inventory,
895
925
  Boolean(orderType === ORDER_TYPES.ARRIVAL_NOTICE) ? arrivalNotice : returnOrder,
926
+ INVENTORY_TRANSACTION_TYPE.UNDO_UNLOADING,
896
927
  -inventory.qty,
897
928
  -inventory.uomValue,
898
- INVENTORY_TRANSACTION_TYPE.UNDO_UNLOADING
929
+ this.user,
930
+ this.trxMgr
899
931
  )
900
932
  inventory.qty = 0
901
933
  inventory.uomValue = 0
@@ -909,14 +941,23 @@ export class UnloadingWorksheetController extends VasWorksheetController {
909
941
  await this.trxMgr.getRepository(Inventory).delete({ id: inventory.id })
910
942
  }
911
943
  } else {
944
+ await this.trxMgr.getRepository(Inventory).createQueryBuilder()
945
+ .update(Inventory).set({
946
+ lastSeq: () => `"last_seq" + 1`,
947
+ status: INVENTORY_STATUS.DELETED,
948
+ updater: this.user, updatedAt: new Date()
949
+ })
950
+ .where('id = :id', { id: inventory.id }).execute()
912
951
  inventory.lastSeq++
913
952
  inventory.status = INVENTORY_STATUS.DELETED
914
- inventory = await this.transactionInventory(
953
+ await generateInventoryHistory(
915
954
  inventory,
916
955
  Boolean(orderType === ORDER_TYPES.ARRIVAL_NOTICE) ? arrivalNotice : returnOrder,
956
+ INVENTORY_TRANSACTION_TYPE.UNDO_UNLOADING,
917
957
  -inventory.qty,
918
958
  -inventory.uomValue,
919
- INVENTORY_TRANSACTION_TYPE.UNDO_UNLOADING
959
+ this.user,
960
+ this.trxMgr
920
961
  )
921
962
  inventory.qty = 0
922
963
  inventory.uomValue = 0
@@ -1096,12 +1137,14 @@ export class UnloadingWorksheetController extends VasWorksheetController {
1096
1137
  }
1097
1138
 
1098
1139
  for (const inventory of inventories) {
1099
- await this.transactionInventory(
1140
+ await generateInventoryHistory(
1100
1141
  inventory,
1101
1142
  arrivalNotice,
1143
+ INVENTORY_TRANSACTION_TYPE.UNLOADING,
1102
1144
  inventory.qty,
1103
1145
  inventory.uomValue,
1104
- INVENTORY_TRANSACTION_TYPE.UNLOADING
1146
+ this.user,
1147
+ this.trxMgr
1105
1148
  )
1106
1149
  }
1107
1150
  } catch (e) {
@@ -18,6 +18,7 @@ import {
18
18
  } from '@things-factory/sales-base'
19
19
  import { webhookHandler, WebhookEventsEnum } from '@things-factory/integration-base'
20
20
  import {
21
+ generateInventoryHistory,
21
22
  Inventory,
22
23
  INVENTORY_ITEM_SOURCE,
23
24
  INVENTORY_STATUS,
@@ -296,7 +297,7 @@ export class PackingWorksheetController extends VasWorksheetController {
296
297
  await this.trxMgr.getRepository(InventoryItem).save(inventoryItems)
297
298
  }
298
299
 
299
- await this.transactionInventory(inventory, releaseGood, 0, 0, INVENTORY_TRANSACTION_TYPE.PACKING)
300
+ await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.PACKING, 0, 0, this.user, this.trxMgr)
300
301
 
301
302
  worksheetDetail.status = WORKSHEET_STATUS.DONE
302
303
  }
@@ -484,7 +485,7 @@ export class PackingWorksheetController extends VasWorksheetController {
484
485
 
485
486
  if (orderInventory.packedQty === releaseQty) {
486
487
  orderInventory.status = ORDER_INVENTORY_STATUS.PACKED
487
- await this.transactionInventory(inventory, releaseGood, 0, 0, INVENTORY_TRANSACTION_TYPE.PACKING)
488
+ await generateInventoryHistory(inventory, releaseGood, INVENTORY_TRANSACTION_TYPE.PACKING, 0, 0, this.user, this.trxMgr)
488
489
  await this.trxMgr.getRepository(WorksheetDetail).update(
489
490
  { targetInventory: { id: orderInventory.id }, type: 'PACKING' },
490
491
  {
@@ -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
 
@@ -1371,10 +1414,31 @@ export class PickingWorksheetController extends VasWorksheetController {
1371
1414
  if (!toLocation) throw new Error(this.ERROR_MSG.FIND.NO_RESULT(locationName))
1372
1415
 
1373
1416
  if (fromLocation.id !== toLocation.id) {
1417
+ await this.trxMgr
1418
+ .getRepository(Inventory)
1419
+ .update(
1420
+ { id: inventory.id },
1421
+ {
1422
+ location: toLocation,
1423
+ warehouse: toLocation.warehouse,
1424
+ zone: toLocation.zone,
1425
+ updater: this.user
1426
+ }
1427
+ )
1428
+
1374
1429
  inventory.location = toLocation
1375
1430
  inventory.warehouse = toLocation.warehouse
1376
1431
  inventory.zone = toLocation.zone
1377
- inventory = await this.transactionInventory(inventory, releaseGood, 0, 0, INVENTORY_TRANSACTION_TYPE.RELOCATE)
1432
+
1433
+ await generateInventoryHistory(
1434
+ inventory,
1435
+ releaseGood,
1436
+ INVENTORY_TRANSACTION_TYPE.RELOCATE,
1437
+ 0,
1438
+ 0,
1439
+ this.user,
1440
+ this.trxMgr
1441
+ )
1378
1442
  }
1379
1443
  }
1380
1444
  } catch (error) {
@@ -1426,6 +1490,16 @@ export class PickingWorksheetController extends VasWorksheetController {
1426
1490
  if (sumOfReleaseQty != releaseQty)
1427
1491
  throw new Error(this.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `insufficient picking quantity`))
1428
1492
 
1493
+ // validation to prevent decimal quantities for non-decimal products
1494
+ if (targetInventories.length > 0) {
1495
+ const batchPickProduct: Product = await this.trxMgr.getRepository(Product).findOne({
1496
+ where: { id: targetInventories[0].productId }
1497
+ })
1498
+ if (releaseQty % 1 !== 0 && !batchPickProduct?.isInventoryDecimal) {
1499
+ throw new Error('Decimal quantities are not allowed for this product')
1500
+ }
1501
+ }
1502
+
1429
1503
  for (var i = 0; i < targetInventories.length; i++) {
1430
1504
  let targetInventory: OrderInventory = targetInventories[i]
1431
1505
  let inventory: Inventory = await this.trxMgr.getRepository(Inventory).findOne({
@@ -1480,10 +1554,12 @@ export class PickingWorksheetController extends VasWorksheetController {
1480
1554
  await this.updateOrderTargets([targetInventory])
1481
1555
 
1482
1556
  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}`,
1557
+ qty: () => `"qty" - :deductQty::numeric`,
1558
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
1559
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
1560
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
1561
+ status: () =>
1562
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1487
1563
  updater: this.user,
1488
1564
  updatedAt: new Date()
1489
1565
  }
@@ -1493,6 +1569,8 @@ export class PickingWorksheetController extends VasWorksheetController {
1493
1569
  .createQueryBuilder()
1494
1570
  .update(Inventory)
1495
1571
  .set(updateInvObj)
1572
+ .setParameter('deductQty', targetInventory.releaseQty)
1573
+ .setParameter('deductUomValue', targetInventory.releaseUomValue)
1496
1574
  .where('id = :id', { id: targetInventory.inventory.id })
1497
1575
  .returning(['qty'])
1498
1576
  .execute()
@@ -1521,10 +1599,31 @@ export class PickingWorksheetController extends VasWorksheetController {
1521
1599
  if (!toLocation) throw new Error(this.ERROR_MSG.FIND.NO_RESULT(locationName))
1522
1600
 
1523
1601
  if (fromLocation.id !== toLocation.id) {
1602
+ await this.trxMgr
1603
+ .getRepository(Inventory)
1604
+ .update(
1605
+ { id: inventory.id },
1606
+ {
1607
+ location: toLocation,
1608
+ warehouse: toLocation.warehouse,
1609
+ zone: toLocation.zone,
1610
+ updater: this.user
1611
+ }
1612
+ )
1613
+
1524
1614
  inventory.location = toLocation
1525
1615
  inventory.warehouse = toLocation.warehouse
1526
1616
  inventory.zone = toLocation.zone
1527
- await this.transactionInventory(inventory, releaseGood, 0, 0, INVENTORY_TRANSACTION_TYPE.RELOCATE)
1617
+
1618
+ await generateInventoryHistory(
1619
+ inventory,
1620
+ releaseGood,
1621
+ INVENTORY_TRANSACTION_TYPE.RELOCATE,
1622
+ 0,
1623
+ 0,
1624
+ this.user,
1625
+ this.trxMgr
1626
+ )
1528
1627
  }
1529
1628
  }
1530
1629
  }
@@ -1599,6 +1698,11 @@ export class PickingWorksheetController extends VasWorksheetController {
1599
1698
 
1600
1699
  pickedQty = matchingProduct.qty
1601
1700
 
1701
+ // validation to prevent decimal quantities for non-decimal products
1702
+ if (pickedQty % 1 !== 0 && !product?.isInventoryDecimal) {
1703
+ throw new Error('Decimal quantities are not allowed for this product')
1704
+ }
1705
+
1602
1706
  const sumOfReleaseQty: number = parseFloat(
1603
1707
  targetInventories
1604
1708
  .map((oi: OrderInventory) => oi.releaseQty)
@@ -1678,10 +1782,12 @@ export class PickingWorksheetController extends VasWorksheetController {
1678
1782
  await this.updateOrderTargets([targetInventory])
1679
1783
 
1680
1784
  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}`,
1785
+ qty: () => `"qty" - :deductQty::numeric`,
1786
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
1787
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
1788
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
1789
+ status: () =>
1790
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
1685
1791
  updater: this.user,
1686
1792
  updatedAt: new Date()
1687
1793
  }
@@ -1691,6 +1797,8 @@ export class PickingWorksheetController extends VasWorksheetController {
1691
1797
  .createQueryBuilder()
1692
1798
  .update(Inventory)
1693
1799
  .set(updateInvObj)
1800
+ .setParameter('deductQty', targetInventory.releaseQty)
1801
+ .setParameter('deductUomValue', targetInventory.releaseUomValue)
1694
1802
  .where('id = :id', { id: targetInventory.inventory.id })
1695
1803
  .returning(['qty'])
1696
1804
  .execute()
@@ -1966,6 +2074,14 @@ export class PickingWorksheetController extends VasWorksheetController {
1966
2074
  }
1967
2075
 
1968
2076
  private async updatePickingTransaction(releaseGood, orderInventory, worksheetDetail, inventory, pickedQty) {
2077
+ // validation to prevent decimal quantities for non-decimal products
2078
+ const pickTxProduct: Product = await this.trxMgr.getRepository(Product).findOne({
2079
+ where: { id: orderInventory.productId }
2080
+ })
2081
+ if (pickedQty % 1 !== 0 && !pickTxProduct?.isInventoryDecimal) {
2082
+ throw new Error('Decimal quantities are not allowed for this product')
2083
+ }
2084
+
1969
2085
  const releaseQty: number = orderInventory.releaseQty
1970
2086
 
1971
2087
  orderInventory.pickedQty = (orderInventory?.pickedQty || 0) + pickedQty
@@ -1980,18 +2096,40 @@ export class PickingWorksheetController extends VasWorksheetController {
1980
2096
  orderInventory.pickedByUser = this.user
1981
2097
  orderInventory.pickedAt = new Date()
1982
2098
 
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(
2099
+ // Atomic SQL update instead of stale-read pattern
2100
+ await this.trxMgr
2101
+ .getRepository(Inventory)
2102
+ .createQueryBuilder()
2103
+ .update(Inventory)
2104
+ .set({
2105
+ qty: () => `"qty" - :deductQty::numeric`,
2106
+ uomValue: () => `"uom_value" - :deductUomValue::numeric`,
2107
+ lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
2108
+ lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
2109
+ status: () =>
2110
+ `case when "qty" - :deductQty::numeric <= 0 then '${INVENTORY_STATUS.TERMINATED}' else status end`,
2111
+ updater: this.user,
2112
+ updatedAt: new Date()
2113
+ })
2114
+ .setParameter('deductQty', orderInventory.releaseQty)
2115
+ .setParameter('deductUomValue', orderInventory.releaseUomValue)
2116
+ .where('id = :id', { id: inventory.id })
2117
+ .execute()
2118
+
2119
+ // Generate inventory history separately
2120
+ await generateInventoryHistory(
1988
2121
  inventory,
1989
2122
  releaseGood,
2123
+ INVENTORY_TRANSACTION_TYPE.PICKING,
1990
2124
  -orderInventory.releaseQty,
1991
2125
  -orderInventory.releaseUomValue,
1992
- INVENTORY_TRANSACTION_TYPE.PICKING
2126
+ this.user,
2127
+ this.trxMgr
1993
2128
  )
1994
2129
 
2130
+ // Re-read inventory for downstream use (status check for TERMINATED)
2131
+ inventory = await this.trxMgr.getRepository(Inventory).findOne({ where: { id: inventory.id } })
2132
+
1995
2133
  worksheetDetail.status = WORKSHEET_STATUS.DONE
1996
2134
  worksheetDetail.updater = this.user
1997
2135
  await this.trxMgr.getRepository(WorksheetDetail).save(worksheetDetail)
@@ -2545,20 +2683,37 @@ export class PickingWorksheetController extends VasWorksheetController {
2545
2683
  // update inventory locked qty and uom value
2546
2684
  oi = await transaction.getRepository(OrderInventory).save({ ...oi })
2547
2685
 
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
- })
2686
+ const lockResult = await transaction
2687
+ .getRepository(Inventory)
2688
+ .createQueryBuilder()
2689
+ .update(Inventory)
2690
+ .set({
2691
+ lockedQty: () => `COALESCE("locked_qty", 0) + :releaseQty::numeric`,
2692
+ lockedUomValue: () => `COALESCE("locked_uom_value", 0) + :releaseUomValue::numeric`,
2693
+ updater: this.user
2694
+ })
2695
+ .setParameter('releaseQty', oi.releaseQty)
2696
+ .setParameter('releaseUomValue', oi.releaseUomValue)
2697
+ .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
2698
+ id: oi.inventory.id,
2699
+ newQty: oi.releaseQty
2700
+ })
2701
+ .execute()
2702
+
2703
+ if (lockResult.affected === 0) {
2704
+ throw new Error(`Insufficient inventory for picking assignment`)
2705
+ }
2553
2706
 
2554
2707
  await transaction
2555
2708
  .getRepository(ProductDetailStock)
2556
2709
  .createQueryBuilder()
2557
2710
  .update(ProductDetailStock)
2558
2711
  .set({
2559
- unassignedQty: () => `"unassigned_qty" - ${oi.releaseQty}`,
2560
- unassignedUomValue: () => `"unassigned_uom_value" - ${oi.releaseUomValue}`
2712
+ unassignedQty: () => `GREATEST("unassigned_qty" - :oiReleaseQty::numeric, 0)`,
2713
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :oiReleaseUomValue::numeric, 0)`
2561
2714
  })
2715
+ .setParameter('oiReleaseQty', oi.releaseQty)
2716
+ .setParameter('oiReleaseUomValue', oi.releaseUomValue)
2562
2717
  .where({ productDetail: oi.productDetail.id })
2563
2718
  .execute()
2564
2719
 
@@ -2643,26 +2798,37 @@ export class PickingWorksheetController extends VasWorksheetController {
2643
2798
  releaseUomValue = releaseUomValue - allocatedUomValue
2644
2799
 
2645
2800
  //// Update inventory locked quantity
2646
- await this.trxMgr
2801
+ const lockResult = await this.trxMgr
2647
2802
  .getRepository(Inventory)
2648
2803
  .createQueryBuilder('inv')
2649
2804
  .update(Inventory)
2650
2805
  .set({
2651
- lockedUomValue: () => `COALESCE(locked_uom_value,0) + ${allocatedUomValue}`,
2652
- lockedQty: () => `COALESCE(locked_qty,0) + ${allocatedQty}`
2806
+ lockedUomValue: () => `COALESCE(locked_uom_value,0) + :allocatedUomValue::numeric`,
2807
+ lockedQty: () => `COALESCE(locked_qty,0) + :allocatedQty::numeric`
2808
+ })
2809
+ .setParameter('allocatedUomValue', allocatedUomValue)
2810
+ .setParameter('allocatedQty', allocatedQty)
2811
+ .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
2812
+ id: targetInventory.id,
2813
+ newQty: allocatedQty
2653
2814
  })
2654
- .where('id = :id', { id: targetInventory.id })
2655
2815
  .execute()
2656
2816
 
2817
+ if (lockResult.affected === 0) {
2818
+ throw new Error(`Insufficient inventory for picking assignment`)
2819
+ }
2820
+
2657
2821
  // update product detail stock deduct unassigned qty and unassigned uom value
2658
2822
  await this.trxMgr
2659
2823
  .getRepository(ProductDetailStock)
2660
2824
  .createQueryBuilder()
2661
2825
  .update(ProductDetailStock)
2662
2826
  .set({
2663
- unassignedQty: () => `"unassigned_qty" - ${allocatedQty}`,
2664
- unassignedUomValue: () => `"unassigned_uom_value" - ${allocatedUomValue}`
2827
+ unassignedQty: () => `GREATEST("unassigned_qty" - :deductQty::numeric, 0)`,
2828
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :deductUomValue::numeric, 0)`
2665
2829
  })
2830
+ .setParameter('deductQty', allocatedQty)
2831
+ .setParameter('deductUomValue', allocatedUomValue)
2666
2832
  .where({ productDetail: orderProducts[i].productDetail.id })
2667
2833
  .execute()
2668
2834