@things-factory/warehouse-base 4.2.12 → 4.3.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 (41) hide show
  1. package/dist-server/constants/index.js +1 -0
  2. package/dist-server/constants/index.js.map +1 -1
  3. package/dist-server/constants/tote.js +9 -0
  4. package/dist-server/constants/tote.js.map +1 -0
  5. package/dist-server/index.js +3 -1
  6. package/dist-server/index.js.map +1 -1
  7. package/dist-server/service/index.js +7 -2
  8. package/dist-server/service/index.js.map +1 -1
  9. package/dist-server/service/inventory/inventory-query.js +2 -1
  10. package/dist-server/service/inventory/inventory-query.js.map +1 -1
  11. package/dist-server/service/inventory-change/inventory-change-mutation.js +334 -273
  12. package/dist-server/service/inventory-change/inventory-change-mutation.js.map +1 -1
  13. package/dist-server/service/inventory-history/inventory-history-query.js +23 -2
  14. package/dist-server/service/inventory-history/inventory-history-query.js.map +1 -1
  15. package/dist-server/service/tote/index.js +9 -0
  16. package/dist-server/service/tote/index.js.map +1 -0
  17. package/dist-server/service/tote/tote-mutation.js +192 -0
  18. package/dist-server/service/tote/tote-mutation.js.map +1 -0
  19. package/dist-server/service/tote/tote-query.js +146 -0
  20. package/dist-server/service/tote/tote-query.js.map +1 -0
  21. package/dist-server/service/tote/tote-types.js +75 -0
  22. package/dist-server/service/tote/tote-types.js.map +1 -0
  23. package/dist-server/service/tote/tote.js +98 -0
  24. package/dist-server/service/tote/tote.js.map +1 -0
  25. package/dist-server/utils/inventory-util.js +5 -3
  26. package/dist-server/utils/inventory-util.js.map +1 -1
  27. package/package.json +8 -8
  28. package/server/constants/index.ts +1 -0
  29. package/server/constants/tote.ts +5 -0
  30. package/server/index.ts +2 -0
  31. package/server/service/index.ts +7 -2
  32. package/server/service/inventory/inventory-query.ts +2 -1
  33. package/server/service/inventory-change/index.ts +1 -1
  34. package/server/service/inventory-change/inventory-change-mutation.ts +471 -407
  35. package/server/service/inventory-history/inventory-history-query.ts +23 -2
  36. package/server/service/tote/index.ts +6 -0
  37. package/server/service/tote/tote-mutation.ts +200 -0
  38. package/server/service/tote/tote-query.ts +102 -0
  39. package/server/service/tote/tote-types.ts +44 -0
  40. package/server/service/tote/tote.ts +77 -0
  41. package/server/utils/inventory-util.ts +5 -3
@@ -1,11 +1,12 @@
1
1
  import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
2
2
  import { EntityManager, In, MoreThan, Not } from 'typeorm'
3
3
 
4
- import { User } from '@things-factory/auth-base'
4
+ import { Privilege, User } from '@things-factory/auth-base'
5
5
  import { Bizplace } from '@things-factory/biz-base'
6
6
  import { generateId } from '@things-factory/id-rule-base'
7
7
  import { MarketplaceStore } from '@things-factory/integration-marketplace'
8
8
  import { Sellercraft, SellercraftStatus } from '@things-factory/integration-sellercraft'
9
+ import { sendNotification } from '@things-factory/notification'
9
10
  import { Product } from '@things-factory/product-base'
10
11
  import { PartnerSetting, Setting } from '@things-factory/setting-base'
11
12
  import { Domain } from '@things-factory/shell'
@@ -69,7 +70,6 @@ export class InventoryChangeMutation {
69
70
  @Ctx() context: any
70
71
  ): Promise<InventoryChange[]> {
71
72
  const { domain, user, tx }: { domain: Domain; user: User; tx: EntityManager } = context.state
72
-
73
73
  let results = []
74
74
  const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
75
75
  const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
@@ -152,6 +152,14 @@ export class InventoryChangeMutation {
152
152
  })
153
153
  }
154
154
 
155
+ const invLockedQty: number = foundExistingPallet?.lockedQty == null ? 0 : foundExistingPallet?.lockedQty
156
+
157
+ if (patches[i]?.qty < invLockedQty) {
158
+ throw new Error(
159
+ 'Adjusted qty value should not be lower than released qty, kindly contact our support if you have difficulties'
160
+ )
161
+ }
162
+
155
163
  if (foundExistingPallet) {
156
164
  delete patches[i].serialNumbers
157
165
  _updateRecords.push(patches[i])
@@ -286,8 +294,11 @@ export class InventoryChangeMutation {
286
294
  product = existingRecord.product
287
295
  }
288
296
 
297
+ updateRecord.transactionType == 'MISSING'
298
+ ? (updateRecord.transactionType = 'MISSING')
299
+ : (updateRecord.transactionType = 'CHANGES')
300
+
289
301
  updateRecord.status = 'PENDING'
290
- updateRecord.transactionType = 'CHANGES'
291
302
 
292
303
  updateRecord.expirationDate =
293
304
  ((updateRecord?.expirationDate && new Date(updateRecord.expirationDate).getFullYear()) || 0) < 2000
@@ -345,6 +356,47 @@ export class InventoryChangeMutation {
345
356
  }
346
357
  }
347
358
 
359
+ const notificationApprover: any = await tx
360
+ .getRepository(Privilege)
361
+ .createQueryBuilder('p')
362
+ .select('u.*')
363
+ .innerJoin('p.roles', 'r')
364
+ .innerJoin('r.users', 'u')
365
+ .where('p.name = :name', { name: 'mutation' })
366
+ .andWhere('p.category = :category', { category: 'inventory' })
367
+ .andWhere('r.domain_id = :domainId', { domainId: domain.id })
368
+ .groupBy('u.id')
369
+ .getRawMany()
370
+ if (_updateRecords.some(res => res.transactionType == 'MISSING')) {
371
+ if (notificationApprover?.length && context.header?.referer) {
372
+ const receivers: any[] = notificationApprover.map(user => user.id)
373
+ const msg = {
374
+ title: `Missing stock identified. Review this transaction in Inventory Adjustment Approval.`,
375
+ body: ``,
376
+ url: context.header.referer,
377
+ data: { url: context.header.referer }
378
+ }
379
+ await sendNotification({
380
+ receivers,
381
+ message: { ...msg }
382
+ })
383
+ }
384
+ } else {
385
+ if (notificationApprover?.length && context.header?.referer) {
386
+ const receivers: any[] = notificationApprover.map(user => user.id)
387
+ const msg = {
388
+ title: `There is an inventory adjustment pending for approval, kindly review within 1 hour to avoid any operation conflict.`,
389
+ body: ``,
390
+ url: context.header.referer,
391
+ data: { url: context.header.referer }
392
+ }
393
+ await sendNotification({
394
+ receivers,
395
+ message: { ...msg }
396
+ })
397
+ }
398
+ }
399
+
348
400
  return true
349
401
  }
350
402
 
@@ -355,411 +407,9 @@ export class InventoryChangeMutation {
355
407
  @Arg('patches', type => [InventoryChangePatch]) patches: InventoryChangePatch[],
356
408
  @Ctx() context: any
357
409
  ): Promise<InventoryChangeList> {
358
- const { domain, user, tx }: { domain: Domain; user: User; tx: EntityManager } = context.state
359
-
360
- // Get Selected Inventory Change Data
361
- const _inventoryChanges = await tx.getRepository(InventoryChange).find({
362
- where: { id: In(patches.map(item => item.id)) },
363
- relations: [
364
- 'inventory',
365
- 'inventory.bizplace',
366
- 'inventory.bizplace.domain',
367
- 'inventory.product',
368
- 'inventory.location',
369
- 'inventory.warehouse',
370
- 'bizplace',
371
- 'bizplace.domain',
372
- 'bizplace.company',
373
- 'bizplace.company.domain',
374
- 'product',
375
- 'product.productDetails',
376
- 'product.productDetails.childProductDetail',
377
- 'location'
378
- ],
379
- order: {
380
- createdAt: 'ASC'
381
- }
382
- })
383
-
384
- let arrLockedInventory: InventoryChange[] = []
385
-
386
- if (_inventoryChanges.length > 0) {
387
- let today = new Date(),
388
- year = today.getFullYear(),
389
- month = today.getMonth(),
390
- date = today.getDate()
391
-
392
- for (let i = 0; i < _inventoryChanges.length; i++) {
393
- if (_inventoryChanges[i].status.toLocaleLowerCase() != 'pending') continue
394
-
395
- const newRecord: any = { ..._inventoryChanges[i] },
396
- newHistoryRecord: any = { ..._inventoryChanges[i] }
397
-
398
- // Adjustment of existing Inventory
399
- if (_inventoryChanges[i].inventory != null) {
400
- let inventoryId = _inventoryChanges[i].inventory.id
401
- let inventory: Inventory = await tx.getRepository(Inventory).findOne({
402
- where: { id: inventoryId },
403
- relations: [
404
- 'domain',
405
- 'bizplace',
406
- 'bizplace.domain',
407
- 'bizplace.company',
408
- 'bizplace.company.domain',
409
- 'product',
410
- 'product.productDetails',
411
- 'product.productDetails.childProductDetail',
412
- 'warehouse',
413
- 'location',
414
- 'creator',
415
- 'updater'
416
- ]
417
- })
418
-
419
- const customerDomain: Domain = inventory.bizplace.domain
420
-
421
- // Check for locked inventory, and stop from udpating inventory
422
- if (inventory.lockedQty > 0) {
423
- arrLockedInventory.push(_inventoryChanges[i])
424
- continue
425
- }
426
-
427
- // Check for Terminated inventory, then update InventoryChange entity, and stop from udpating inventory
428
- if (inventory.status == INVENTORY_STATUS.TERMINATED) {
429
- _inventoryChanges[i] = { ..._inventoryChanges[i], status: 'TERMINATED' }
430
- continue
431
- }
432
-
433
- let lastSeq = inventory.lastSeq,
434
- transactionType = ''
435
-
436
- newHistoryRecord.openingQty = inventory.qty
437
- newHistoryRecord.openingUomValue = inventory.uomValue
438
-
439
- // Get last row of InventoryHistory
440
- let latestEntry = await tx.getRepository(InventoryHistory).findOne({
441
- where: { palletId: inventory.palletId, domain: domain },
442
- order: { seq: 'DESC' }
443
- })
444
- _inventoryChanges[i].lastInventoryHistory = latestEntry
445
-
446
- // Check Change of existing inventory location
447
- if (newRecord.location && newRecord.location.id != inventory.location.id) {
448
- newRecord.zone = newRecord.location.zone
449
- newRecord.warehouse = newRecord.location.warehouse
450
- transactionType = 'ADJUSTMENT'
451
-
452
- // Check and set current location status
453
- let currentLocationInventoryCount = await tx.getRepository(Inventory).count({
454
- where: { location: inventory.location, status: 'STORED', id: Not(inventory.id) }
455
- })
456
-
457
- if (currentLocationInventoryCount == 0) {
458
- let currentLocation = await tx.getRepository(Location).findOne({
459
- where: { id: inventory.location.id }
460
- })
461
- await tx.getRepository(Location).save({
462
- ...currentLocation,
463
- status: LOCATION_STATUS.EMPTY
464
- })
465
- }
466
- }
467
-
468
- // Check Change of existing inventory quantity
469
- if (newRecord.qty != null && newRecord.qty != inventory.qty) {
470
- newHistoryRecord.qty = newRecord.qty - inventory.qty
471
- if (newRecord.qty < 1) {
472
- newRecord.qty = 0
473
- newRecord.uomValue = 0
474
- }
475
- transactionType = 'ADJUSTMENT'
476
- } else {
477
- newHistoryRecord.qty = 0
478
- }
479
-
480
- // Check Change of existing inventory uomValue
481
- if (newRecord.uomValue != null && newRecord.uomValue != inventory.uomValue) {
482
- newHistoryRecord.uomValue = newRecord.uomValue - inventory.uomValue
483
- Math.round(newHistoryRecord.uomValue * 100) / 100
484
- transactionType = 'ADJUSTMENT'
485
- } else {
486
- newHistoryRecord.uomValue = 0
487
- }
488
-
489
- // Terminate current inventory history if there is change of bizplace, product, batchId, packingType, or uom
490
- if (
491
- (newRecord.bizplace && inventory.bizplace.id !== newRecord.bizplace.id) ||
492
- (newRecord.product && inventory.product.id !== newRecord.product.id) ||
493
- (newRecord.batchId && inventory.batchId !== newRecord.batchId) ||
494
- (newRecord.packingType && inventory.packingType !== newRecord.packingType) ||
495
- (newRecord.uom && inventory.uom !== newRecord.uom)
496
- ) {
497
- transactionType = 'ADJUSTMENT'
498
- lastSeq = lastSeq + 1
499
- let inventoryHistory = {
500
- ...inventory,
501
- domain: domain,
502
- bizplace: inventory.bizplace.id,
503
- openingQty: inventory.qty,
504
- openingUomValue: inventory.uomValue,
505
- qty: -inventory.qty || 0,
506
- uom: inventory.uom,
507
- uomValue: -inventory.uomValue || 0,
508
- name: InventoryNoGenerator.inventoryHistoryName(),
509
- seq: lastSeq,
510
- transactionType: transactionType,
511
- status: INVENTORY_STATUS.TERMINATED,
512
- productId: inventory.product.id,
513
- warehouseId: inventory.warehouse.id,
514
- locationId: inventory.location.id,
515
- packingType: inventory.packingType,
516
- creator: user,
517
- updater: user
518
- }
519
- delete inventoryHistory.id
520
- delete inventoryHistory.createdAt
521
- delete inventoryHistory.updatedAt
522
- await tx.getRepository(InventoryHistory).save(inventoryHistory)
523
- newHistoryRecord.qty = newRecord.qty != null ? newRecord.qty : inventory.qty || 0
524
- newHistoryRecord.uomValue = newRecord.uomValue != null ? newRecord.uomValue : inventory.uomValue || 0
525
- newHistoryRecord.openingQty = 0
526
- newHistoryRecord.openingUomValue = 0
527
- }
528
-
529
- // Set and update inventory and inventory history data
530
- lastSeq = lastSeq + 1
531
- clean(newHistoryRecord)
532
- let inventoryHistory = {
533
- ...inventory,
534
- ...newHistoryRecord,
535
- domain: domain,
536
- creator: user,
537
- updater: user,
538
- name: InventoryNoGenerator.inventoryHistoryName(),
539
- status: 'STORED',
540
- seq: lastSeq,
541
- transactionType: transactionType == '' ? 'ADJUSTMENT' : transactionType,
542
- productId: newRecord.product ? newRecord.product.id : inventory.product.id,
543
- warehouseId: newRecord.warehouse ? newRecord.warehouse.id : inventory.warehouse.id,
544
- locationId:
545
- newRecord.location && newRecord.location.id != inventory.location.id
546
- ? newRecord.location.id
547
- : inventory.location.id,
548
- uom: newRecord.uom != null ? newRecord.uom : inventory.uom
549
- }
550
- delete inventoryHistory.id
551
- delete inventoryHistory.createdAt
552
- delete inventoryHistory.updatedAt
553
- await tx.getRepository(InventoryHistory).save(inventoryHistory)
554
-
555
- if (newRecord.qty != null && newRecord.qty == 0) {
556
- ++lastSeq
557
- delete inventoryHistory.id
558
- inventoryHistory = {
559
- ...inventoryHistory,
560
- name: InventoryNoGenerator.inventoryHistoryName(),
561
- qty: 0,
562
- uomValue: 0,
563
- openingQty: 0,
564
- openingUomValue: 0,
565
- seq: lastSeq,
566
- transactionType: 'TERMINATED',
567
- status: 'TERMINATED'
568
- }
569
-
570
- await tx.getRepository(InventoryHistory).save(inventoryHistory)
571
- }
410
+ const inventoryChangeList = await approveInventoryChanges(patches, context)
572
411
 
573
- clean(newRecord)
574
- await tx.getRepository(Inventory).save({
575
- ...inventory,
576
- ...newRecord,
577
- id: inventoryId,
578
- status: (newRecord.qty != null ? newRecord.qty : inventory.qty) > 0 ? 'STORED' : 'TERMINATED',
579
- updater: user,
580
- lastSeq: lastSeq
581
- })
582
-
583
- //Check and set latest location status
584
- if (newRecord.qty != null ? newRecord.qty : inventory.qty > 0) {
585
- var location = await tx.getRepository(Location).findOne({
586
- where: { id: newRecord.location ? newRecord.location.id : inventory.location.id }
587
- })
588
- await tx.getRepository(Location).save({
589
- ...location,
590
- status: LOCATION_STATUS.OCCUPIED
591
- })
592
- } else {
593
- let latestLocationInventoryCount = await tx.getRepository(Inventory).count({
594
- where: {
595
- location: newRecord.location ? newRecord.location.id : inventory.location.id,
596
- status: 'STORED',
597
- id: Not(inventory.id)
598
- }
599
- })
600
- if (latestLocationInventoryCount == 0) {
601
- let latestLocation = await tx.getRepository(Location).findOne({
602
- where: { id: newRecord.location ? newRecord.location.id : inventory.location.id }
603
- })
604
- await tx.getRepository(Location).save({
605
- ...latestLocation,
606
- status: LOCATION_STATUS.EMPTY
607
- })
608
- }
609
- }
610
-
611
- await upsertInventoryItems(context, _inventoryChanges[i], 'APPROVED', tx)
612
-
613
- const sellercraft: Sellercraft = await tx
614
- .getRepository(Sellercraft)
615
- .findOne({ domain: customerDomain, status: SellercraftStatus.ACTIVE })
616
-
617
- const companyDomain: Domain = inventory.bizplace?.company.domain
618
-
619
- if (sellercraft) {
620
- const sellercraftCtrl: SellercraftController = new SellercraftController(tx, domain, user)
621
- await sellercraftCtrl.updateSellercraftStock(sellercraft, inventory)
622
- }
623
-
624
- // check for any existing active marketplace connection, update marketplace selling qty
625
- const marketplaceStores: MarketplaceStore[] = await tx.getRepository(MarketplaceStore).find({
626
- where: { domain: companyDomain, status: 'ACTIVE', isAutoUpdateStockQty: true },
627
- relations: ['marketplaceDistributors']
628
- })
629
-
630
- if (marketplaceStores?.length && marketplaceStores.some(store => store.isAutoUpdateStockQty)) {
631
- const ecommerceCtrl: EcommerceController = new EcommerceController(tx, domain, user)
632
- await ecommerceCtrl.updateProductVariationStock(marketplaceStores, inventory.product.id, companyDomain)
633
- }
634
- }
635
-
636
- // Adding Inventory
637
- else {
638
- const total = await tx.getRepository(Inventory).count({
639
- createdAt: MoreThan(new Date(year, month, date))
640
- })
641
-
642
- const yy = String(year).substr(String(year).length - 2)
643
- const mm = String(month + 1).padStart(2, '0')
644
- const dd = String(date).padStart(2, '0')
645
-
646
- const dateStr = yy + mm + dd
647
-
648
- let palletId = newRecord.palletId
649
-
650
- if (!newRecord.palletId?.trim()) {
651
- palletId = await generateId({
652
- domain: domain,
653
- type: 'adjustment_pallet_id',
654
- seed: {
655
- batchId: newRecord.batchId,
656
- date: dateStr
657
- }
658
- })
659
- if (!palletId) throw new Error('No adjustment pallet id setting rule found')
660
- }
661
-
662
- var location = await tx.getRepository(Location).findOne({
663
- where: { id: newRecord.location.id },
664
- relations: ['warehouse']
665
- })
666
- newRecord.location = location
667
- newRecord.zone = location.zone
668
- newRecord.warehouse = location.warehouse
669
-
670
- newRecord.status = INVENTORY_STATUS.STORED
671
- newRecord.name = palletId
672
- newRecord.palletId = palletId
673
-
674
- const warehouseCartonSetting: Setting = await tx.getRepository(Setting).findOne({
675
- where: { domain, category: 'id-rule', name: 'enable-carton-label' }
676
- })
677
-
678
- const partnerCartonSetting: PartnerSetting = await tx.getRepository(PartnerSetting).findOne({
679
- where: { setting: warehouseCartonSetting, domain, partnerDomain: newRecord.bizplace?.domain }
680
- })
681
-
682
- if (partnerCartonSetting?.value) {
683
- let cartonId = await generateId({
684
- domain: domain,
685
- type: 'adjustment_carton_id',
686
- seed: { date: dateStr }
687
- })
688
- if (!cartonId) throw new Error('No adjustment carton id setting rule found')
689
-
690
- newRecord.cartonId = cartonId
691
- }
692
-
693
- let expirationDate =
694
- ((newRecord?.expirationDate && new Date(newRecord.expirationDate).getFullYear()) || 0) < 2000
695
- ? null
696
- : newRecord.expirationDate
697
-
698
- let savedInventory = await tx.getRepository(Inventory).save({
699
- ...newRecord,
700
- expirationDate,
701
- domain: domain,
702
- creator: user,
703
- updater: user,
704
- lastSeq: 0
705
- })
706
-
707
- await tx.getRepository(InventoryHistory).save({
708
- ...newRecord,
709
- domain: domain,
710
- creator: user,
711
- updater: user,
712
- name: InventoryNoGenerator.inventoryHistoryName(),
713
- seq: 0,
714
- transactionType: 'NEW',
715
- productId: newRecord.product.id,
716
- warehouseId: newRecord.warehouse.id,
717
- locationId: newRecord.location.id,
718
- inventory: savedInventory,
719
- expirationDate: expirationDate
720
- })
721
-
722
- await tx.getRepository(Location).save({
723
- ...location,
724
- status: LOCATION_STATUS.OCCUPIED
725
- })
726
-
727
- _inventoryChanges[i].inventory = savedInventory
728
- _inventoryChanges[i].palletId = savedInventory.palletId
729
-
730
- await upsertInventoryItems(context, _inventoryChanges[i], 'APPROVED', tx)
731
-
732
- const sellercraft: Sellercraft = await tx
733
- .getRepository(Sellercraft)
734
- .findOne({ domain: newRecord.bizplace.domain, status: SellercraftStatus.ACTIVE })
735
-
736
- const companyDomain: Domain = newRecord.bizplace?.company.domain
737
-
738
- if (sellercraft) {
739
- const sellercraftCtrl: SellercraftController = new SellercraftController(tx, domain, user)
740
- await sellercraftCtrl.updateSellercraftStock(sellercraft, newRecord)
741
- }
742
-
743
- // update marketplace store qty
744
- // check for any existing active marketplace connection, update marketplace selling qty
745
- const marketplaceStores: MarketplaceStore[] = await tx.getRepository(MarketplaceStore).find({
746
- where: { domain: companyDomain, status: 'ACTIVE', isAutoUpdateStockQty: true },
747
- relations: ['marketplaceDistributors']
748
- })
749
-
750
- if (marketplaceStores?.length && marketplaceStores.some(store => store.isAutoUpdateStockQty)) {
751
- const ecommerceCtrl: EcommerceController = new EcommerceController(tx, domain, user)
752
- await ecommerceCtrl.updateProductVariationStock(marketplaceStores, newRecord.product.id, companyDomain)
753
- }
754
- }
755
-
756
- _inventoryChanges[i].status = 'APPROVED'
757
- }
758
-
759
- await tx.getRepository(InventoryChange).save(_inventoryChanges)
760
- }
761
-
762
- return { items: arrLockedInventory, total: arrLockedInventory.length }
412
+ return inventoryChangeList
763
413
  }
764
414
 
765
415
  @Directive('@privilege(category: "inventory_adjustment_approval", privilege: "mutation")')
@@ -883,3 +533,417 @@ async function upsertInventoryItems(context, inventoryChange, approvalStatus, tx
883
533
  })
884
534
  )
885
535
  }
536
+
537
+ export async function approveInventoryChanges(patches: any, context: any) {
538
+ const { domain, user, tx }: { domain: Domain; user: User; tx: EntityManager } = context.state
539
+
540
+ // Get Selected Inventory Change Data
541
+ const _inventoryChanges = await tx.getRepository(InventoryChange).find({
542
+ where: { id: In(patches.map(item => item.id)) },
543
+ relations: [
544
+ 'inventory',
545
+ 'inventory.bizplace',
546
+ 'inventory.bizplace.domain',
547
+ 'inventory.product',
548
+ 'inventory.location',
549
+ 'inventory.warehouse',
550
+ 'bizplace',
551
+ 'bizplace.domain',
552
+ 'bizplace.company',
553
+ 'bizplace.company.domain',
554
+ 'product',
555
+ 'product.productDetails',
556
+ 'product.productDetails.childProductDetail',
557
+ 'location'
558
+ ],
559
+ order: {
560
+ createdAt: 'ASC'
561
+ }
562
+ })
563
+
564
+ let arrLockedInventory: InventoryChange[] = []
565
+
566
+ if (_inventoryChanges.length > 0) {
567
+ let today = new Date(),
568
+ year = today.getFullYear(),
569
+ month = today.getMonth(),
570
+ date = today.getDate()
571
+
572
+ for (let i = 0; i < _inventoryChanges.length; i++) {
573
+ if (_inventoryChanges[i].status.toLocaleLowerCase() != 'pending') continue
574
+
575
+ const newRecord: any = { ..._inventoryChanges[i] },
576
+ newHistoryRecord: any = { ..._inventoryChanges[i] }
577
+
578
+ // Adjustment of existing Inventory
579
+ if (_inventoryChanges[i].inventory != null) {
580
+ let inventoryId = _inventoryChanges[i].inventory.id
581
+ let inventory: Inventory = await tx.getRepository(Inventory).findOne({
582
+ where: { id: inventoryId },
583
+ relations: [
584
+ 'domain',
585
+ 'bizplace',
586
+ 'bizplace.domain',
587
+ 'bizplace.company',
588
+ 'bizplace.company.domain',
589
+ 'product',
590
+ 'product.productDetails',
591
+ 'product.productDetails.childProductDetail',
592
+ 'warehouse',
593
+ 'location',
594
+ 'creator',
595
+ 'updater'
596
+ ]
597
+ })
598
+
599
+ const customerDomain: Domain = inventory.bizplace.domain
600
+
601
+ // Check for locked inventory, and stop from udpating inventory
602
+ if (inventory.qty < inventory.lockedQty) {
603
+ arrLockedInventory.push(_inventoryChanges[i])
604
+ continue
605
+ }
606
+
607
+ // Check for Terminated inventory, then update InventoryChange entity, and stop from udpating inventory
608
+ if (inventory.status == INVENTORY_STATUS.TERMINATED) {
609
+ _inventoryChanges[i] = { ..._inventoryChanges[i], status: 'TERMINATED' }
610
+ continue
611
+ }
612
+
613
+ let lastSeq = inventory.lastSeq,
614
+ transactionType = ''
615
+
616
+ newHistoryRecord.openingQty = inventory.qty
617
+ newHistoryRecord.openingUomValue = inventory.uomValue
618
+
619
+ // Get last row of InventoryHistory
620
+ let latestEntry = await tx.getRepository(InventoryHistory).findOne({
621
+ where: { palletId: inventory.palletId, domain: domain },
622
+ order: { seq: 'DESC' }
623
+ })
624
+ _inventoryChanges[i].lastInventoryHistory = latestEntry
625
+
626
+ // Check Change of existing inventory location
627
+ if (newRecord.location && newRecord.location.id != inventory.location.id) {
628
+ newRecord.zone = newRecord.location.zone
629
+ newRecord.warehouse = newRecord.location.warehouse
630
+ transactionType = 'ADJUSTMENT'
631
+
632
+ // Check and set current location status
633
+ let currentLocationInventoryCount = await tx.getRepository(Inventory).count({
634
+ where: { location: inventory.location, status: 'STORED', id: Not(inventory.id) }
635
+ })
636
+
637
+ if (currentLocationInventoryCount == 0) {
638
+ let currentLocation = await tx.getRepository(Location).findOne({
639
+ where: { id: inventory.location.id }
640
+ })
641
+ await tx.getRepository(Location).save({
642
+ ...currentLocation,
643
+ status: LOCATION_STATUS.EMPTY
644
+ })
645
+ }
646
+ }
647
+
648
+ // Check Change of existing inventory quantity
649
+ if (newRecord.qty != null && newRecord.qty != inventory.qty) {
650
+ newHistoryRecord.qty = newRecord.qty - inventory.qty
651
+ if (newRecord.qty < 1) {
652
+ newRecord.qty = 0
653
+ newRecord.uomValue = 0
654
+ }
655
+ transactionType = 'ADJUSTMENT'
656
+ } else {
657
+ newHistoryRecord.qty = 0
658
+ }
659
+
660
+ // Check Change of existing inventory uomValue
661
+ if (newRecord.uomValue != null && newRecord.uomValue != inventory.uomValue) {
662
+ newHistoryRecord.uomValue = newRecord.uomValue - inventory.uomValue
663
+ Math.round(newHistoryRecord.uomValue * 100) / 100
664
+ transactionType = 'ADJUSTMENT'
665
+ } else {
666
+ newHistoryRecord.uomValue = 0
667
+ }
668
+
669
+ // Terminate current inventory history if there is change of bizplace, product, batchId, packingType, or uom
670
+ if (
671
+ (newRecord.bizplace && inventory.bizplace.id !== newRecord.bizplace.id) ||
672
+ (newRecord.product && inventory.product.id !== newRecord.product.id) ||
673
+ (newRecord.batchId && inventory.batchId !== newRecord.batchId) ||
674
+ (newRecord.packingType && inventory.packingType !== newRecord.packingType) ||
675
+ (newRecord.uom && inventory.uom !== newRecord.uom)
676
+ ) {
677
+ transactionType = 'ADJUSTMENT'
678
+ lastSeq = lastSeq + 1
679
+ let inventoryHistory = {
680
+ ...inventory,
681
+ domain: domain,
682
+ bizplace: inventory.bizplace.id,
683
+ openingQty: inventory.qty,
684
+ openingUomValue: inventory.uomValue,
685
+ qty: -inventory.qty || 0,
686
+ uom: inventory.uom,
687
+ uomValue: -inventory.uomValue || 0,
688
+ name: InventoryNoGenerator.inventoryHistoryName(),
689
+ seq: lastSeq,
690
+ transactionType: transactionType,
691
+ status: INVENTORY_STATUS.TERMINATED,
692
+ productId: inventory.product.id,
693
+ warehouseId: inventory.warehouse.id,
694
+ locationId: inventory.location.id,
695
+ packingType: inventory.packingType,
696
+ creator: user,
697
+ updater: user
698
+ }
699
+ delete inventoryHistory.id
700
+ delete inventoryHistory.createdAt
701
+ delete inventoryHistory.updatedAt
702
+ await tx.getRepository(InventoryHistory).save(inventoryHistory)
703
+ newHistoryRecord.qty = newRecord.qty != null ? newRecord.qty : inventory.qty || 0
704
+ newHistoryRecord.uomValue = newRecord.uomValue != null ? newRecord.uomValue : inventory.uomValue || 0
705
+ newHistoryRecord.openingQty = 0
706
+ newHistoryRecord.openingUomValue = 0
707
+ }
708
+
709
+ // Set and update inventory and inventory history data
710
+ lastSeq = lastSeq + 1
711
+ clean(newHistoryRecord)
712
+ let inventoryHistory = {
713
+ ...inventory,
714
+ ...newHistoryRecord,
715
+ domain: domain,
716
+ creator: user,
717
+ updater: user,
718
+ name: InventoryNoGenerator.inventoryHistoryName(),
719
+ status: newHistoryRecord.transactionType == 'MISSING' ? 'MISSING' : 'STORED',
720
+ seq: lastSeq,
721
+ transactionType: transactionType == '' ? 'ADJUSTMENT' : transactionType,
722
+ productId: newRecord.product ? newRecord.product.id : inventory.product.id,
723
+ warehouseId: newRecord.warehouse ? newRecord.warehouse.id : inventory.warehouse.id,
724
+ locationId:
725
+ newRecord.location && newRecord.location.id != inventory.location.id
726
+ ? newRecord.location.id
727
+ : inventory.location.id,
728
+ uom: newRecord.uom != null ? newRecord.uom : inventory.uom
729
+ }
730
+ delete inventoryHistory.id
731
+ delete inventoryHistory.createdAt
732
+ delete inventoryHistory.updatedAt
733
+ await tx.getRepository(InventoryHistory).save(inventoryHistory)
734
+
735
+ if (newRecord.qty != null && newRecord.qty == 0) {
736
+ ++lastSeq
737
+ delete inventoryHistory.id
738
+ inventoryHistory = {
739
+ ...inventoryHistory,
740
+ name: InventoryNoGenerator.inventoryHistoryName(),
741
+ qty: 0,
742
+ uomValue: 0,
743
+ openingQty: 0,
744
+ openingUomValue: 0,
745
+ seq: lastSeq,
746
+ transactionType: 'TERMINATED',
747
+ status: 'TERMINATED'
748
+ }
749
+
750
+ await tx.getRepository(InventoryHistory).save(inventoryHistory)
751
+ }
752
+
753
+ clean(newRecord)
754
+ let statusFilter = ''
755
+ if (newRecord.transactionType == 'MISSING') {
756
+ statusFilter = 'MISSING'
757
+ } else {
758
+ statusFilter = (newRecord.qty != null ? newRecord.qty : inventory.qty) > 0 ? 'STORED' : 'TERMINATED'
759
+ }
760
+ await tx.getRepository(Inventory).save({
761
+ ...inventory,
762
+ ...newRecord,
763
+ id: inventoryId,
764
+ status: statusFilter,
765
+ updater: user,
766
+ lastSeq: lastSeq
767
+ })
768
+
769
+ //Check and set latest location status
770
+ if (newRecord.qty != null ? newRecord.qty : inventory.qty > 0) {
771
+ var location = await tx.getRepository(Location).findOne({
772
+ where: { id: newRecord.location ? newRecord.location.id : inventory.location.id }
773
+ })
774
+ await tx.getRepository(Location).save({
775
+ ...location,
776
+ status: LOCATION_STATUS.OCCUPIED
777
+ })
778
+ } else {
779
+ let latestLocationInventoryCount = await tx.getRepository(Inventory).count({
780
+ where: {
781
+ location: newRecord.location ? newRecord.location.id : inventory.location.id,
782
+ status: 'STORED',
783
+ id: Not(inventory.id)
784
+ }
785
+ })
786
+ if (latestLocationInventoryCount == 0) {
787
+ let latestLocation = await tx.getRepository(Location).findOne({
788
+ where: { id: newRecord.location ? newRecord.location.id : inventory.location.id }
789
+ })
790
+ await tx.getRepository(Location).save({
791
+ ...latestLocation,
792
+ status: LOCATION_STATUS.EMPTY
793
+ })
794
+ }
795
+ }
796
+
797
+ await upsertInventoryItems(context, _inventoryChanges[i], 'APPROVED', tx)
798
+
799
+ const sellercraft: Sellercraft = await tx
800
+ .getRepository(Sellercraft)
801
+ .findOne({ domain: customerDomain, status: SellercraftStatus.ACTIVE })
802
+
803
+ const companyDomain: Domain = inventory.bizplace?.company.domain
804
+
805
+ if (sellercraft) {
806
+ const sellercraftCtrl: SellercraftController = new SellercraftController(tx, domain, user)
807
+ await sellercraftCtrl.updateSellercraftStock(sellercraft, inventory)
808
+ }
809
+
810
+ // check for any existing active marketplace connection, update marketplace selling qty
811
+ const marketplaceStores: MarketplaceStore[] = await tx.getRepository(MarketplaceStore).find({
812
+ where: { domain: companyDomain, status: 'ACTIVE', isAutoUpdateStockQty: true },
813
+ relations: ['marketplaceDistributors']
814
+ })
815
+
816
+ if (marketplaceStores?.length && marketplaceStores.some(store => store.isAutoUpdateStockQty)) {
817
+ const ecommerceCtrl: EcommerceController = new EcommerceController(tx, domain, user)
818
+ await ecommerceCtrl.updateProductVariationStock(marketplaceStores, inventory.product.id, companyDomain)
819
+ }
820
+ }
821
+
822
+ // Adding Inventory
823
+ else {
824
+ const total = await tx.getRepository(Inventory).count({
825
+ createdAt: MoreThan(new Date(year, month, date))
826
+ })
827
+
828
+ const yy = String(year).substr(String(year).length - 2)
829
+ const mm = String(month + 1).padStart(2, '0')
830
+ const dd = String(date).padStart(2, '0')
831
+
832
+ const dateStr = yy + mm + dd
833
+
834
+ let palletId = newRecord.palletId
835
+
836
+ if (!newRecord.palletId?.trim()) {
837
+ palletId = await generateId({
838
+ domain: domain,
839
+ type: 'adjustment_pallet_id',
840
+ seed: {
841
+ batchId: newRecord.batchId,
842
+ date: dateStr
843
+ }
844
+ })
845
+ if (!palletId) throw new Error('No adjustment pallet id setting rule found')
846
+ }
847
+
848
+ var location = await tx.getRepository(Location).findOne({
849
+ where: { id: newRecord.location.id },
850
+ relations: ['warehouse']
851
+ })
852
+ newRecord.location = location
853
+ newRecord.zone = location.zone
854
+ newRecord.warehouse = location.warehouse
855
+
856
+ newRecord.status = INVENTORY_STATUS.STORED
857
+ newRecord.name = palletId
858
+ newRecord.palletId = palletId
859
+
860
+ const warehouseCartonSetting: Setting = await tx.getRepository(Setting).findOne({
861
+ where: { domain, category: 'id-rule', name: 'enable-carton-label' }
862
+ })
863
+
864
+ const partnerCartonSetting: PartnerSetting = await tx.getRepository(PartnerSetting).findOne({
865
+ where: { setting: warehouseCartonSetting, domain, partnerDomain: newRecord.bizplace?.domain }
866
+ })
867
+
868
+ if (partnerCartonSetting?.value) {
869
+ let cartonId = await generateId({
870
+ domain: domain,
871
+ type: 'adjustment_carton_id',
872
+ seed: { date: dateStr }
873
+ })
874
+ if (!cartonId) throw new Error('No adjustment carton id setting rule found')
875
+
876
+ newRecord.cartonId = cartonId
877
+ }
878
+
879
+ let expirationDate =
880
+ ((newRecord?.expirationDate && new Date(newRecord.expirationDate).getFullYear()) || 0) < 2000
881
+ ? null
882
+ : newRecord.expirationDate
883
+
884
+ let savedInventory = await tx.getRepository(Inventory).save({
885
+ ...newRecord,
886
+ expirationDate,
887
+ domain: domain,
888
+ creator: user,
889
+ updater: user,
890
+ lastSeq: 0
891
+ })
892
+
893
+ await tx.getRepository(InventoryHistory).save({
894
+ ...newRecord,
895
+ domain: domain,
896
+ creator: user,
897
+ updater: user,
898
+ name: InventoryNoGenerator.inventoryHistoryName(),
899
+ seq: 0,
900
+ transactionType: 'NEW',
901
+ productId: newRecord.product.id,
902
+ warehouseId: newRecord.warehouse.id,
903
+ locationId: newRecord.location.id,
904
+ inventory: savedInventory,
905
+ expirationDate: expirationDate
906
+ })
907
+
908
+ await tx.getRepository(Location).save({
909
+ ...location,
910
+ status: LOCATION_STATUS.OCCUPIED
911
+ })
912
+
913
+ _inventoryChanges[i].inventory = savedInventory
914
+ _inventoryChanges[i].palletId = savedInventory.palletId
915
+
916
+ await upsertInventoryItems(context, _inventoryChanges[i], 'APPROVED', tx)
917
+
918
+ const sellercraft: Sellercraft = await tx
919
+ .getRepository(Sellercraft)
920
+ .findOne({ domain: newRecord.bizplace.domain, status: SellercraftStatus.ACTIVE })
921
+
922
+ const companyDomain: Domain = newRecord.bizplace?.company.domain
923
+
924
+ if (sellercraft) {
925
+ const sellercraftCtrl: SellercraftController = new SellercraftController(tx, domain, user)
926
+ await sellercraftCtrl.updateSellercraftStock(sellercraft, newRecord)
927
+ }
928
+
929
+ // update marketplace store qty
930
+ // check for any existing active marketplace connection, update marketplace selling qty
931
+ const marketplaceStores: MarketplaceStore[] = await tx.getRepository(MarketplaceStore).find({
932
+ where: { domain: companyDomain, status: 'ACTIVE', isAutoUpdateStockQty: true },
933
+ relations: ['marketplaceDistributors']
934
+ })
935
+
936
+ if (marketplaceStores?.length && marketplaceStores.some(store => store.isAutoUpdateStockQty)) {
937
+ const ecommerceCtrl: EcommerceController = new EcommerceController(tx, domain, user)
938
+ await ecommerceCtrl.updateProductVariationStock(marketplaceStores, newRecord.product.id, companyDomain)
939
+ }
940
+ }
941
+
942
+ _inventoryChanges[i].status = 'APPROVED'
943
+ }
944
+
945
+ await tx.getRepository(InventoryChange).save(_inventoryChanges)
946
+ }
947
+
948
+ return { items: arrLockedInventory, total: arrLockedInventory.length }
949
+ }