From d334f4682944963d7128b453921a1e80b61f74c9 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 4 Mar 2026 12:41:26 +0700 Subject: [PATCH] [FEAT/BE] wiring recording,transfer_stock,transfer_laying,marketing for consumer chick in project flock population --- .../fifo_stock_v2/population_allocation.go | 131 ++++++++++++++++++ ...8090010_seed_fifo_stock_v2_config.down.sql | 3 +- ...218090010_seed_fifo_stock_v2_config.up.sql | 4 +- ...033546_create_fifo_stock_v2_core.down.sql} | 0 ...04033546_create_fifo_stock_v2_core.up.sql} | 0 ...ed_execution_to_laying_transfers.down.sql} | 4 + ...rred_execution_to_laying_transfers.up.sql} | 4 + .../modules/inventory/transfers/module.go | 3 +- .../transfers/services/transfer.service.go | 69 ++++++++- internal/modules/marketing/module.go | 3 +- .../services/deliveryorder.service.go | 74 +++++++++- .../modules/production/chickins/module.go | 3 + .../chickins/services/chickin.service.go | 48 ++++++- .../modules/production/recordings/module.go | 1 + .../recordings/services/recording.service.go | 77 ++++++++++ .../services/transfer_laying.service.go | 43 ++++++ .../purchases/services/purchase.service.go | 57 +++++++- 17 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 internal/common/service/fifo_stock_v2/population_allocation.go rename internal/database/migrations/{20260218090000_create_fifo_stock_v2_core.down.sql => 20260304033546_create_fifo_stock_v2_core.down.sql} (100%) rename internal/database/migrations/{20260218090000_create_fifo_stock_v2_core.up.sql => 20260304033546_create_fifo_stock_v2_core.up.sql} (100%) rename internal/database/migrations/{20260227130000_add_deferred_execution_to_laying_transfers.down.sql => 20260304033605_add_deferred_execution_to_laying_transfers.down.sql} (96%) rename internal/database/migrations/{20260227130000_add_deferred_execution_to_laying_transfers.up.sql => 20260304033605_add_deferred_execution_to_laying_transfers.up.sql} (98%) diff --git a/internal/common/service/fifo_stock_v2/population_allocation.go b/internal/common/service/fifo_stock_v2/population_allocation.go new file mode 100644 index 00000000..ce961564 --- /dev/null +++ b/internal/common/service/fifo_stock_v2/population_allocation.go @@ -0,0 +1,131 @@ +package fifo_stock_v2 + +import ( + "context" + "errors" + "math" + "sort" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func ReleasePopulationConsumptionByUsable( + ctx context.Context, + tx *gorm.DB, + usableType string, + usableID uint, +) error { + if tx == nil { + return errors.New("transaction is required") + } + if usableType == "" || usableID == 0 { + return errors.New("usable type and id are required") + } + + stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx) + allocations, err := stockAllocationRepo.FindActiveByUsable(ctx, usableType, usableID, nil) + if err != nil { + return err + } + + for _, allocation := range allocations { + if allocation.StockableType != fifo.StockableKeyProjectFlockPopulation.String() || allocation.StockableId == 0 || allocation.Qty <= 0 { + continue + } + if err := tx.WithContext(ctx). + Model(&entity.ProjectFlockPopulation{}). + Where("id = ?", allocation.StockableId). + Update("total_used_qty", gorm.Expr("GREATEST(total_used_qty - ?, 0)", allocation.Qty)).Error; err != nil { + return err + } + } + + return stockAllocationRepo.ReleaseByUsable(ctx, usableType, usableID, nil, nil) +} + +func AllocatePopulationConsumption( + ctx context.Context, + tx *gorm.DB, + populations []entity.ProjectFlockPopulation, + productWarehouseID uint, + usableType string, + usableID uint, + consumeQty float64, +) error { + if consumeQty <= 0 { + return nil + } + if tx == nil { + return errors.New("transaction is required") + } + if productWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak valid") + } + if usableType == "" || usableID == 0 { + return errors.New("usable type and id are required") + } + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan") + } + + if err := ReleasePopulationConsumptionByUsable(ctx, tx, usableType, usableID); err != nil { + return err + } + + sort.Slice(populations, func(i, j int) bool { + if populations[i].CreatedAt.Equal(populations[j].CreatedAt) { + return populations[i].Id < populations[j].Id + } + return populations[i].CreatedAt.Before(populations[j].CreatedAt) + }) + + stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx) + remaining := consumeQty + for _, pop := range populations { + available := pop.TotalQty - pop.TotalUsedQty + if available <= 0 { + continue + } + portion := math.Min(available, remaining) + if portion <= 0 { + continue + } + + allocation := &entity.StockAllocation{ + ProductWarehouseId: productWarehouseID, + StockableType: fifo.StockableKeyProjectFlockPopulation.String(), + StockableId: pop.Id, + UsableType: usableType, + UsableId: usableID, + Qty: portion, + Status: entity.StockAllocationStatusActive, + AllocationPurpose: entity.StockAllocationPurposeConsume, + } + if err := stockAllocationRepo.CreateOne(ctx, allocation, nil); err != nil { + return err + } + + if err := tx.WithContext(ctx). + Model(&entity.ProjectFlockPopulation{}). + Where("id = ?", pop.Id). + Update("total_used_qty", gorm.Expr("total_used_qty + ?", portion)).Error; err != nil { + return err + } + + remaining -= portion + if remaining <= 1e-6 { + break + } + } + + if remaining > 1e-6 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak mencukupi") + } + + return nil +} diff --git a/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.down.sql b/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.down.sql index a9fb727b..05786a61 100644 --- a/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.down.sql +++ b/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.down.sql @@ -24,7 +24,8 @@ WHERE source_table IN ( 'recording_depletions', 'recording_eggs', 'marketing_delivery_products', - 'project_chickins' + 'project_chickins', + 'project_flock_populations' ); DELETE FROM fifo_stock_v2_flag_members diff --git a/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.up.sql b/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.up.sql index e6914e94..f791aebf 100644 --- a/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.up.sql +++ b/internal/database/migrations/20260218090010_seed_fifo_stock_v2_config.up.sql @@ -79,7 +79,8 @@ VALUES ('marketing_delivery_products', 'USABLE', NULL, NULL, NULL, 'delivery_date', 'created_at', 45, 'id'), - ('project_chickins', 'USABLE', NULL, NULL, NULL, 'chick_in_date', 'created_at', 50, 'id') + ('project_chickins', 'USABLE', NULL, NULL, NULL, 'chick_in_date', 'created_at', 50, 'id'), + ('project_flock_populations', 'STOCKABLE', 'project_chickins', 'project_chickin_id', 'id', 'chick_in_date', 'created_at', 55, 'id') ON CONFLICT (source_table, lane) DO UPDATE SET date_table = EXCLUDED.date_table, @@ -112,6 +113,7 @@ VALUES ('AYAM', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE), ('AYAM', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'PURCHASE_ITEMS', TRUE, TRUE), ('AYAM', 'STOCKABLE', 'TRANSFER_TO_LAYING_IN', 'laying_transfer_targets', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'TRANSFERTOLAYING_IN', TRUE, TRUE), + ('AYAM', 'STOCKABLE', 'POPULATION_IN', 'project_flock_populations', 'id', 'product_warehouse_id', 'total_qty', 'total_used_qty', NULL, NULL, 'PROJECT_FLOCK_POPULATION', TRUE, TRUE), -- AYAM USABLE ('AYAM', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE), diff --git a/internal/database/migrations/20260218090000_create_fifo_stock_v2_core.down.sql b/internal/database/migrations/20260304033546_create_fifo_stock_v2_core.down.sql similarity index 100% rename from internal/database/migrations/20260218090000_create_fifo_stock_v2_core.down.sql rename to internal/database/migrations/20260304033546_create_fifo_stock_v2_core.down.sql diff --git a/internal/database/migrations/20260218090000_create_fifo_stock_v2_core.up.sql b/internal/database/migrations/20260304033546_create_fifo_stock_v2_core.up.sql similarity index 100% rename from internal/database/migrations/20260218090000_create_fifo_stock_v2_core.up.sql rename to internal/database/migrations/20260304033546_create_fifo_stock_v2_core.up.sql diff --git a/internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.down.sql b/internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.down.sql similarity index 96% rename from internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.down.sql rename to internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.down.sql index 1b4d26c0..6b9143f2 100644 --- a/internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.down.sql +++ b/internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.down.sql @@ -1,3 +1,5 @@ +BEGIN; + DROP INDEX IF EXISTS idx_laying_transfers_executed_by; DROP INDEX IF EXISTS idx_laying_transfers_executed_at; DROP INDEX IF EXISTS idx_laying_transfers_effective_move_date; @@ -9,3 +11,5 @@ ALTER TABLE laying_transfers DROP COLUMN IF EXISTS executed_by, DROP COLUMN IF EXISTS executed_at, DROP COLUMN IF EXISTS effective_move_date; + +COMMIT; diff --git a/internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.up.sql b/internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.up.sql similarity index 98% rename from internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.up.sql rename to internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.up.sql index 2fa1b39e..b90c7a3a 100644 --- a/internal/database/migrations/20260227130000_add_deferred_execution_to_laying_transfers.up.sql +++ b/internal/database/migrations/20260304033605_add_deferred_execution_to_laying_transfers.up.sql @@ -1,3 +1,5 @@ +BEGIN; + ALTER TABLE laying_transfers ADD COLUMN IF NOT EXISTS effective_move_date DATE, ADD COLUMN IF NOT EXISTS executed_at TIMESTAMPTZ, @@ -44,3 +46,5 @@ WHERE ( ORDER BY a.id DESC LIMIT 1 ) = 'APPROVED'; + +COMMIT; diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 50498493..e45e0dd9 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -39,6 +39,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db) kandangRepo := rKandang.NewKandangRepository(db) nonstockRepo := rNonstock.NewNonstockRepository(db) documentRepo := commonRepo.NewDocumentRepository(db) @@ -76,7 +77,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate expenseServiceInstance, ) - transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoStockV2Service, expenseBridge) + transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, projectFlockPopulationRepo, documentSvc, fifoStockV2Service, expenseBridge) userService := sUser.NewUserService(userRepo, validate) TransferRoutes(router, userService, transferService) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index fd7749ee..9cf4789e 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -11,6 +11,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -21,6 +22,7 @@ import ( projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) @@ -43,12 +45,13 @@ type transferService struct { SupplierRepo rSupplier.SupplierRepository WarehouseRepo warehouseRepo.WarehouseRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository + ProjectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository DocumentSvc commonSvc.DocumentService FifoStockV2Svc commonSvc.FifoStockV2Service ExpenseBridge TransferExpenseBridge } -func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService { +func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService { return &transferService{ Log: utils.Log, Validate: validate, @@ -61,6 +64,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr SupplierRepo: supplierRepo, WarehouseRepo: warehouseRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, + ProjectFlockPopulationRepo: projectFlockPopulationRepo, DocumentSvc: documentSvc, FifoStockV2Svc: fifoStockV2Svc, ExpenseBridge: expenseBridge, @@ -509,6 +513,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal", product.ProductID)) } + if strings.EqualFold(flagGroupCode, "AYAM") && outUsageQty > 0 { + if err := s.allocatePopulationForStockTransferOut( + c.Context(), + tx, + detail, + uint(*detail.SourceProductWarehouseID), + outUsageQty, + ); err != nil { + return err + } + } + stockLogDecrease := &entity.StockLog{ ProductWarehouseId: uint(*detail.SourceProductWarehouseID), CreatedBy: uint(actorID), @@ -617,6 +633,57 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return result, nil } +func (s *transferService) allocatePopulationForStockTransferOut( + ctx context.Context, + tx *gorm.DB, + detail *entity.StockTransferDetail, + sourceProductWarehouseID uint, + consumeQty float64, +) error { + if consumeQty <= 0 { + return nil + } + if tx == nil { + return errors.New("transaction is required") + } + if detail == nil || detail.Id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Data transfer detail tidak valid") + } + if sourceProductWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Gudang sumber tidak valid") + } + + pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, sourceProductWarehouseID, nil) + if err != nil { + return err + } + if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == 0 { + return nil + } + + populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID( + ctx, + *pw.ProjectFlockKandangId, + sourceProductWarehouseID, + ) + if err != nil { + return err + } + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk transfer") + } + + return fifoV2.AllocatePopulationConsumption( + ctx, + tx, + populations, + sourceProductWarehouseID, + fifo.UsableKeyStockTransferOut.String(), + uint(detail.Id), + consumeQty, + ) +} + func (s *transferService) resolveTransferFlagGroup( ctx context.Context, tx *gorm.DB, diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go index 649c0363..ce48a06e 100644 --- a/internal/modules/marketing/module.go +++ b/internal/modules/marketing/module.go @@ -31,6 +31,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db) stockLogRepo := rShared.NewStockLogRepository(db) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) @@ -46,7 +47,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoStockV2Service, warehouseRepo, projectFlockKandangRepo, validate) - deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoStockV2Service, validate) + deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, productWarehouseRepo, projectFlockPopulationRepo, approvalSvc, fifoStockV2Service, validate) userService := sUser.NewUserService(userRepo, validate) RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index c90e2873..d9f485f9 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -4,17 +4,22 @@ import ( "context" "errors" "fmt" + "strings" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -34,6 +39,8 @@ type deliveryOrdersService struct { MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository StockLogRepo rShared.StockLogRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ApprovalSvc commonSvc.ApprovalService FifoStockV2Svc commonSvc.FifoStockV2Service } @@ -43,6 +50,8 @@ func NewDeliveryOrdersService( marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, stockLogRepo rShared.StockLogRepository, + productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, + projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, validate *validator.Validate, @@ -53,6 +62,8 @@ func NewDeliveryOrdersService( MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, StockLogRepo: stockLogRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProjectFlockPopulationRepo: projectFlockPopulationRepo, ApprovalSvc: approvalSvc, FifoStockV2Svc: fifoStockV2Svc, } @@ -556,7 +567,6 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - if err := reflowMarketingScope( ctx, s.FifoStockV2Svc, @@ -575,6 +585,10 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor deliveryProduct.PendingQty = refreshed.PendingQty deliveryProduct.CreatedAt = refreshed.CreatedAt + if err := s.allocatePopulationForMarketingDelivery(ctx, tx, deliveryProduct, marketingProduct.ProductWarehouseId); err != nil { + return err + } + allocatedDelta := deliveryProduct.UsageQty - previousUsage if actorID > 0 && allocatedDelta > 0 { decreaseLog := &entity.StockLog{ @@ -642,6 +656,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor deliveryProduct.PendingQty = refreshed.PendingQty deliveryProduct.CreatedAt = refreshed.CreatedAt + if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil { + return err + } + releasedUsage := currentUsage - deliveryProduct.UsageQty if actorID > 0 && releasedUsage > 0 { increaseLog := &entity.StockLog{ @@ -668,3 +686,57 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor return nil } + +func (s deliveryOrdersService) allocatePopulationForMarketingDelivery( + ctx context.Context, + tx *gorm.DB, + deliveryProduct *entity.MarketingDeliveryProduct, + productWarehouseID uint, +) error { + if deliveryProduct == nil || deliveryProduct.Id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Delivery product tidak valid") + } + if tx == nil { + return errors.New("transaction is required") + } + if deliveryProduct.UsageQty <= 0 { + return nil + } + if productWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak ditemukan") + } + + flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID) + if err != nil { + return err + } + if !strings.EqualFold(flagGroupCode, "AYAM") { + return nil + } + + pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil) + if err != nil { + return err + } + if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == 0 { + return nil + } + + populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID(ctx, *pw.ProjectFlockKandangId, productWarehouseID) + if err != nil { + return err + } + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery") + } + + return fifoV2.AllocatePopulationConsumption( + ctx, + tx, + populations, + productWarehouseID, + fifo.UsableKeyMarketingDelivery.String(), + deliveryProduct.Id, + deliveryProduct.UsageQty, + ) +} diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index 5eb8f36b..e87448b2 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -18,6 +18,7 @@ import ( sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -36,6 +37,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) + transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productRepo := rProduct.NewProductRepository(db) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) @@ -57,6 +59,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, + transferLayingRepo, validate, fifoStockV2Service) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index ca27fe1c..27ef3743 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -19,6 +19,7 @@ import ( repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" @@ -51,11 +52,12 @@ type chickinService struct { ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectChickinDetailRepo repository.ProjectChickinDetailRepository + TransferLayingRepo rTransferLaying.TransferLayingRepository FifoStockV2Svc commonSvc.FifoStockV2Service StockLogRepo rStockLogs.StockLogRepository } -func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoStockV2Svc commonSvc.FifoStockV2Service) ChickinService { +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, transferLayingRepo rTransferLaying.TransferLayingRepository, validate *validator.Validate, fifoStockV2Svc commonSvc.FifoStockV2Service) ChickinService { return &chickinService{ Log: utils.Log, Validate: validate, @@ -68,6 +70,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectChickinDetailRepo: projectChickinDetailRepo, + TransferLayingRepo: transferLayingRepo, FifoStockV2Svc: fifoStockV2Svc, StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), } @@ -120,11 +123,36 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e return chickin, nil } +func (s chickinService) ensureNotTransferred(ctx context.Context, projectFlockKandangID uint) error { + if projectFlockKandangID == 0 || s.TransferLayingRepo == nil { + return nil + } + + transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + s.Log.Errorf("Failed to resolve transfer laying by source kandang %d: %+v", projectFlockKandangID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying") + } + + if transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() { + return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sudah dipindahkan ke laying") + } + + return nil +} + func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureNotTransferred(c.Context(), req.ProjectFlockKandangId); err != nil { + return nil, err + } + projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") @@ -334,6 +362,17 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, err } + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return nil, err + } + if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil { + return nil, err + } + updateBody := make(map[string]any) if req.ChickInDate != "" { @@ -377,6 +416,10 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { return err } + if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil { + return err + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return err @@ -446,6 +489,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &id, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { return nil, err } + if err := s.ensureNotTransferred(c.Context(), id); err != nil { + return nil, err + } latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil) if err != nil { diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index 93263593..8a6ad158 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -98,6 +98,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate projectFlockKandangRepo, projectFlockPopulationRepo, chickinDetailRepo, + transferLayingRepo, validate, fifoStockV2Service, ) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index a020e378..4ed3c0a5 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -10,6 +10,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -23,6 +24,7 @@ import ( rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + fifo "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording" "github.com/go-playground/validator/v10" @@ -416,6 +418,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.reflowApplyRecordingDepletionsIn(ctx, tx, mappedDepletions); err != nil { return err } + if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, createdRecording.ProjectFlockKandangId); err != nil { + s.Log.Errorf("Failed to resync project flock population usage: %+v", err) + return err + } mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs) if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { @@ -951,6 +957,13 @@ func (s *recordingService) enforceTransferRecordingRoute( return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying") } + if transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() { + return fiber.NewError( + fiber.StatusBadRequest, + "Project flock kandang sudah dipindahkan ke laying", + ) + } + effectiveDate := effectiveTransferDate(transfer) if effectiveDate.IsZero() { return nil @@ -1835,6 +1848,14 @@ func (s *recordingService) reflowApplyRecordingDepletionsOut( } s.logDepletionTrace("reflow_apply:done", *refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, refreshed.UsageQty, refreshed.PendingQty)) + consumeQty := refreshed.UsageQty + if refreshed.PendingQty > 0 { + consumeQty += refreshed.PendingQty + } + if err := s.allocatePopulationForDepletion(ctx, tx, *refreshed, consumeQty); err != nil { + return err + } + logDecrease := refreshed.UsageQty if refreshed.PendingQty > 0 { logDecrease += refreshed.PendingQty @@ -1894,11 +1915,15 @@ func (s *recordingService) reflowResetRecordingDepletionsOut( return errors.New("stock log repository is not available") } logState := newRecordingStockLogState() + stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx) for _, depletion := range depletions { if depletion.Id == 0 { continue } + if err := stockAllocationRepo.ReleaseByUsable(ctx, fifo.UsableKeyRecordingDepletion.String(), depletion.Id, nil, nil); err != nil { + return err + } s.logDepletionTrace("reflow_reset:start", depletion, "") sourceWarehouseID := uint(0) @@ -1979,6 +2004,58 @@ func (s *recordingService) reflowResetRecordingDepletionsOut( return nil } +func (s *recordingService) allocatePopulationForDepletion( + ctx context.Context, + tx *gorm.DB, + depletion entity.RecordingDepletion, + consumeQty float64, +) error { + if consumeQty <= 0 { + return nil + } + if tx == nil { + return errors.New("transaction is required") + } + + sourceWarehouseID := uint(0) + if depletion.SourceProductWarehouseId != nil { + sourceWarehouseID = *depletion.SourceProductWarehouseId + } + if sourceWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan") + } + + var projectFlockKandangID uint + if err := tx.WithContext(ctx). + Table("recordings"). + Select("project_flock_kandangs_id"). + Where("id = ?", depletion.RecordingId). + Scan(&projectFlockKandangID).Error; err != nil { + return err + } + if projectFlockKandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak ditemukan untuk depletion") + } + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx, projectFlockKandangID, sourceWarehouseID) + if err != nil { + return err + } + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk depletion") + } + + return fifoV2.AllocatePopulationConsumption( + ctx, + tx, + populations, + sourceWarehouseID, + fifo.UsableKeyRecordingDepletion.String(), + depletion.Id, + consumeQty, + ) +} + func (s *recordingService) reflowApplyRecordingDepletionsIn( ctx context.Context, tx *gorm.DB, diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index b1ef9440..9a1bf993 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -10,6 +10,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" @@ -1005,6 +1006,9 @@ func (s *transferLayingService) executeApprovedTransferMovement( } movedQty := usageDelta + if err := s.allocatePopulationForTransfer(ctx, tx, source, movedQty); err != nil { + return err + } targetShares := distributeProportionalWithRounding(targets, totalTargetQty, movedQty) for i, target := range targets { roundedQty := math.Round(targetShares[i]) @@ -1104,6 +1108,45 @@ func (s *transferLayingService) executeApprovedTransferMovement( return nil } +func (s *transferLayingService) allocatePopulationForTransfer( + ctx context.Context, + tx *gorm.DB, + source entity.LayingTransferSource, + consumeQty float64, +) error { + if consumeQty <= 0 { + return nil + } + if tx == nil { + return errors.New("transaction is required") + } + if source.SourceProjectFlockKandangId == 0 || source.ProductWarehouseId == nil || *source.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sumber atau product warehouse tidak valid") + } + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID( + ctx, + source.SourceProjectFlockKandangId, + *source.ProductWarehouseId, + ) + if err != nil { + return err + } + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk transfer laying") + } + + return fifoV2.AllocatePopulationConsumption( + ctx, + tx, + populations, + *source.ProductWarehouseId, + fifo.UsableKeyTransferToLayingOut.String(), + source.Id, + consumeQty, + ) +} + func (s *transferLayingService) calculateEffectiveMoveDate(ctx context.Context, sources []entity.LayingTransferSource) (time.Time, error) { if len(sources) == 0 { return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Sumber transfer laying tidak ditemukan") diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 48f31bf0..9c038bdd 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -787,6 +787,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation req.Items[idx].TravelDocumentPath = &uploadedURL } } + lockedIDs := map[uint]struct{}{} if action == entity.ApprovalActionApproved { itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) for i := range purchase.Items { @@ -795,11 +796,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } itemByID[purchase.Items[i].Id] = purchase.Items[i] } - lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items) + locked, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items) if err != nil { return nil, err } - if len(lockedIDs) > 0 { + if len(locked) > 0 { + for id := range locked { + lockedIDs[id] = struct{}{} + } for _, payload := range req.Items { if _, used := lockedIDs[payload.PurchaseItemID]; !used { continue @@ -880,7 +884,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation if receivedQty > item.SubQty { return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty)) } - if receivedQty < item.TotalUsed { + if receivedQty < item.TotalUsed && isReceivingBelowUsedBlocked(item, lockedIDs) { return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be lower than used amount (%.3f)", payload.PurchaseItemID, item.TotalUsed)) } @@ -1659,7 +1663,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload( if *data.Qty <= 0 { return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id)) } - if item.TotalUsed > 0 && *data.Qty < item.TotalUsed { + if item.TotalUsed > 0 && *data.Qty < item.TotalUsed && isReceivingBelowUsedBlocked(&item, nil) { return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed)) } if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving { @@ -1778,6 +1782,51 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref return *provided, nil } +func purchaseItemHasFlag(item *entity.PurchaseItem, flag utils.FlagType) bool { + if item == nil || item.Product == nil { + return false + } + target := utils.NormalizeFlag(string(flag)) + for _, f := range item.Product.Flags { + if utils.NormalizeFlag(f.Name) == target { + return true + } + } + return false +} + +func isReceivingBelowUsedBlocked(item *entity.PurchaseItem, lockedIDs map[uint]struct{}) bool { + if item == nil { + return false + } + if !purchaseItemHasAnyFlag(item, []utils.FlagType{ + utils.FlagPullet, + utils.FlagLayer, + utils.FlagAyamAfkir, + utils.FlagAyamCulling, + utils.FlagAyamMati, + }) { + return false + } + if lockedIDs == nil { + return true + } + _, locked := lockedIDs[item.Id] + return locked +} + +func purchaseItemHasAnyFlag(item *entity.PurchaseItem, flags []utils.FlagType) bool { + if item == nil || item.Product == nil || len(flags) == 0 { + return false + } + for _, flag := range flags { + if purchaseItemHasFlag(item, flag) { + return true + } + } + return false +} + func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity.Purchase) error { if item == nil || item.Id == 0 || s.ApprovalSvc == nil { return nil