From 0a84e427c1693aec10ed39b62727551d403173dd Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 7 Jan 2026 09:27:39 +0700 Subject: [PATCH] FIX[BE]: fixing bug transfer to laying, delet biaya, nominal expesen e, chickin --- ...uct_warehouse_to_laying_transfers.down.sql | 9 ++ ...oduct_warehouse_to_laying_transfers.up.sql | 19 +++ ...ckable_fields_to_laying_transfers.down.sql | 14 ++ ...tockable_fields_to_laying_transfers.up.sql | 30 ++++ internal/entities/laying_transfer.go | 23 ++- .../modules/inventory/adjustments/module.go | 4 +- .../services/adjustment.service.go | 20 +-- .../modules/inventory/transfers/module.go | 7 +- .../transfers/services/transfer.service.go | 11 +- .../modules/production/chickins/module.go | 18 +++ .../project_chickin.repository.go | 11 ++ .../chickins/services/chickin.service.go | 143 ++++++++++++++---- .../services/project_flock_kandang.service.go | 25 ++- .../project_flock_population_repository.go | 17 ++- .../projectflock_kandang.repository.go | 14 ++ .../production/transfer_layings/module.go | 41 +++++ .../services/transfer_laying.service.go | 126 ++++++++------- internal/modules/purchases/module.go | 2 +- .../purchases/services/purchase.service.go | 5 +- .../hpp_per_kandang.repository.go | 2 +- internal/utils/fifo/constants.go | 17 ++- 21 files changed, 432 insertions(+), 126 deletions(-) create mode 100644 internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.down.sql create mode 100644 internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.up.sql create mode 100644 internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.down.sql create mode 100644 internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.up.sql diff --git a/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.down.sql b/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.down.sql new file mode 100644 index 00000000..af4a6477 --- /dev/null +++ b/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.down.sql @@ -0,0 +1,9 @@ +-- Drop foreign key and column +ALTER TABLE laying_transfers +DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id; + +ALTER TABLE laying_transfers +DROP COLUMN IF EXISTS product_warehouse_id; + +-- Drop index +DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id; diff --git a/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.up.sql b/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.up.sql new file mode 100644 index 00000000..7e417ff6 --- /dev/null +++ b/internal/database/migrations/20260106113503_add_product_warehouse_to_laying_transfers.up.sql @@ -0,0 +1,19 @@ +-- Add product_warehouse_id to laying_transfers for FIFO support +ALTER TABLE laying_transfers +ADD COLUMN product_warehouse_id BIGINT; + +-- Add foreign key +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_transfers_product_warehouse_id + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE SET NULL; + END IF; +END $$; + +-- Add index +CREATE INDEX idx_laying_transfers_product_warehouse_id +ON laying_transfers(product_warehouse_id); diff --git a/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.down.sql b/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.down.sql new file mode 100644 index 00000000..391731f2 --- /dev/null +++ b/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.down.sql @@ -0,0 +1,14 @@ +-- Rollback: Remove STOCKABLE fields from laying_transfers + +-- Drop index +DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id; + +-- Drop foreign key constraint +ALTER TABLE laying_transfers +DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id; + +-- Drop columns +ALTER TABLE laying_transfers +DROP COLUMN IF EXISTS dest_product_warehouse_id, +DROP COLUMN IF EXISTS total_qty, +DROP COLUMN IF EXISTS total_used; diff --git a/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.up.sql b/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.up.sql new file mode 100644 index 00000000..7a4ce8a6 --- /dev/null +++ b/internal/database/migrations/20260106140815_add_stockable_fields_to_laying_transfers.up.sql @@ -0,0 +1,30 @@ +-- Add STOCKABLE fields to laying_transfers for destination warehouse +-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable) + +-- Add columns for STOCKABLE role (destination warehouse) +ALTER TABLE laying_transfers +ADD COLUMN dest_product_warehouse_id BIGINT, +ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0, +ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0; + +-- Add foreign key constraint +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id + FOREIGN KEY (dest_product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE SET NULL; + END IF; +END $$; + +-- Add index for performance +CREATE INDEX idx_laying_transfers_dest_product_warehouse_id +ON laying_transfers(dest_product_warehouse_id); + +-- Add comment for documentation +COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role'; +COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role'; +COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role'; +COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role'; diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index dd173042..97a7df12 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -12,18 +12,29 @@ type LayingTransfer struct { FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` TransferDate time.Time `gorm:"type:date;not null"` + + PendingUsageQty *float64 `gorm:"type:numeric(15,3)"` UsageQty *float64 `gorm:"type:numeric(15,3)"` + ProductWarehouseId *uint `gorm:"type:bigint"` // Source PW (PULLET) + + + DestProductWarehouseID *uint `gorm:"column:dest_product_warehouse_id;type:bigint"` // Destination PW (LAYER) + TotalQty float64 `gorm:"column:total_qty;type:numeric(15,3);default:0"` // Total lot introduced to destination + TotalUsed float64 `gorm:"column:total_used;type:numeric(15,3);default:0"` // Already consumed from this lot + Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index"` - FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` - ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` - Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` - Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` - LatestApproval *Approval `gorm:"-" json:"-"` + FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` + ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW + DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + LatestApproval *Approval `gorm:"-" json:"-"` } diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go index 08e556ea..6b137902 100644 --- a/internal/modules/inventory/adjustments/module.go +++ b/internal/modules/inventory/adjustments/module.go @@ -36,7 +36,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) err := fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKey("ADJUSTMENT_IN"), + Key: fifo.StockableKeyAdjustmentIn, Table: "adjustment_stocks", Columns: fifo.StockableColumns{ ID: "id", @@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat } err = fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKey("ADJUSTMENT_OUT"), + Key: fifo.UsableKeyAdjustmentOut, Table: "adjustment_stocks", Columns: fifo.UsableColumns{ ID: "id", diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 47d41648..71b985c2 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -20,6 +20,7 @@ import ( projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" stockLogsRepo "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" ) @@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e projectFlockKandangID = &pfkID } - pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk( - ctx, - uint(req.ProductID), - uint(req.WarehouseID), - projectFlockKandangID, - ) + pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to find product warehouse: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } @@ -143,7 +138,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { - s.Log.Errorf("Failed to create product warehouse: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse") } pw = newPW @@ -163,7 +157,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } - // Create StockLog for history tracking afterQuantity := productWarehouse.Quantity newLog := &entity.StockLog{ LoggableType: string(utils.StockLogTypeAdjustment), @@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return err } - // Create AdjustmentStock record for FIFO tracking adjustmentStock := &entity.AdjustmentStock{ StockLogId: newLog.Id, ProductWarehouseId: productWarehouse.Id, @@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } if transactionType == string(utils.StockLogTransactionTypeIncrease) { - // Adjustment INCREASE → Replenish stock (Stockable) + note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) _, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{ - StockableKey: "ADJUSTMENT_IN", + StockableKey: fifo.StockableKeyAdjustmentIn, StockableID: adjustmentStock.Id, ProductWarehouseID: uint(productWarehouse.Id), Quantity: req.Quantity, @@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } } else { - // Adjustment DECREASE → Consume stock (Usable) _, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{ - UsableKey: "ADJUSTMENT_OUT", + UsableKey: fifo.UsableKeyAdjustmentOut, UsableID: adjustmentStock.Id, ProductWarehouseID: uint(productWarehouse.Id), Quantity: req.Quantity, @@ -230,6 +221,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } // Update ProductWarehouse quantity (for backward compatibility/reporting) + productWarehouse.Quantity = afterQuantity if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 60d1764a..bbb3c4aa 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -43,12 +43,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate panic(err) } - // Initialize FIFO Service fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) - // Register Transfer as Stockable (adds stock to destination warehouse) err = fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKey("STOCK_TRANSFER_IN"), + Key: fifo.StockableKeyStockTransferIn, Table: "stock_transfer_details", Columns: fifo.StockableColumns{ ID: "id", @@ -63,9 +61,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate panic(err) } - // Register Transfer as Usable (consumes stock from source warehouse) err = fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKey("STOCK_TRANSFER_OUT"), + Key: fifo.UsableKeyStockTransferOut, Table: "stock_transfer_details", Columns: fifo.UsableColumns{ ID: "id", diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 1ca35a71..afbb4627 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -21,6 +21,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" ) @@ -337,24 +338,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } } - // Execute FIFO operations for each product for _, product := range req.Products { detail := detailMap[uint64(product.ProductID)] - // Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT) consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ - UsableKey: "STOCK_TRANSFER_OUT", + UsableKey: fifo.UsableKeyStockTransferOut, UsableID: uint(detail.Id), ProductWarehouseID: uint(*detail.SourceProductWarehouseID), Quantity: product.ProductQty, - AllowPending: false, // Don't allow pending, must have actual stock + AllowPending: false, Tx: tx, }) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err)) } - // Update usage tracking fields for source warehouse if err := tx.Model(&entity.StockTransferDetail{}). Where("id = ?", detail.Id). Updates(map[string]interface{}{ @@ -367,7 +365,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques // Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ - StockableKey: "STOCK_TRANSFER_IN", + StockableKey: fifo.StockableKeyStockTransferIn, StockableID: uint(detail.Id), ProductWarehouseID: uint(*detail.DestProductWarehouseID), Quantity: product.ProductQty, @@ -378,7 +376,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err)) } - // Update total tracking fields for destination warehouse if err := tx.Model(&entity.StockTransferDetail{}). Where("id = ?", detail.Id). Updates(map[string]interface{}{ diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index 6c9b8984..0c7c2a09 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -58,6 +58,24 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * } } + + if err := fifoService.RegisterStockable(fifo.StockableConfig{ + Key: fifo.StockableKeyProjectFlockPopulation, + Table: "project_flock_populations", + Columns: fifo.StockableColumns{ + ID: "id", + ProductWarehouseID: "product_warehouse_id", + TotalQuantity: "total_qty", + TotalUsedQuantity: "total_used_qty", + CreatedAt: "created_at", + }, + OrderBy: []string{"created_at ASC", "id ASC"}, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register project flock population stockable workflow: %v", err)) + } + } + approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil { diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index f4cbf5e3..7f56a261 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -17,6 +17,7 @@ type ProjectChickinRepository interface { GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) + UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error } type ChickinRepositoryImpl struct { @@ -123,3 +124,13 @@ func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.C Scan(&result).Error return result, err } + +func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error { + return tx.WithContext(ctx). + Model(&entity.ProjectChickin{}). + Where("id = ?", chickinID). + Updates(map[string]interface{}{ + "usage_qty": usageQty, + "pending_usage_qty": pendingUsageQty, + }).Error +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 8c896aef..02ae12ec 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -214,9 +214,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti pendingQtyMap[existingChickin.ProductWarehouseId] += existingChickin.PendingUsageQty } } - - // CRITICAL: Validate chickins sequentially to prevent over-allocation within the same request - // pendingQtyMap is accumulated as we validate each chickin to ensure total pending doesn't exceed available stock for idx, chickin := range newChikins { pendingQty := pendingQtyMap[chickin.ProductWarehouseId] desiredQty := chickinQtyMap[uint(idx)] @@ -232,8 +229,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti chickinQtyMap[uint(idx)] = availableQty - // ACCUMULATE pending for this product warehouse so NEXT chickin in same request sees it - // This prevents multiple chickins in same request from over-allocating the same stock pendingQtyMap[chickin.ProductWarehouseId] += availableQty } @@ -358,12 +353,15 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { } if chickin.UsageQty > 0 { + + currentUsageQty := chickin.UsageQty + if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil { return err } warehouseDeltas := make(map[uint]float64) - warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty + warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil { s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err) return err @@ -618,7 +616,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti population := &entity.ProjectFlockPopulation{ ProjectChickinId: chickin.Id, ProductWarehouseId: targetPW.Id, - TotalQty: quantityToConvert, + TotalQty: 0, // Will be set by FIFO Replenish TotalUsedQty: 0, Notes: chickin.Notes, CreatedBy: actorID, @@ -634,15 +632,22 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err) } + // Replenish stock to target ProductWarehouse based on source flag + // StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID + if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil { + s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err) + return err + } + totalQuantityAdded += quantityToConvert } - // NOTE: Tidak menambah target ProductWarehouse quantity karena: - // 1. Ayam sudah dipakai (masuk population) - // 2. ProductWarehouse source sudah berkurang saat create chickin (ConsumeChickinStocks) - // 3. Menambah quantity disini akan menyebabkan double count - // - // PULLET/LAYER untuk flock ini akan di-add lewat mekanisme lain (misal: purchase, transfer, dll) + // NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks + // yang dipanggil di atas untuk setiap chickin berdasarkan flag source: + // - DOC → replenish ke PULLET + // - PULLET → replenish ke LAYER + // - LAYER → tidak perlu replenish (sudah final) + // - DOC+PULLET+LAYER → replenish ke dirinya sendiri return nil } @@ -671,10 +676,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d", result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations)) - if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ - "usage_qty": result.UsageQuantity, - "pending_usage_qty": result.PendingQuantity, - }).Error; err != nil { + if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil { return err } @@ -696,6 +698,101 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, return nil } +func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error { + if chickin == nil || targetPW == nil || population == nil || s.FifoSvc == nil { + return nil + } + + sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product.Flags") + }) + if err != nil { + + return err + } + if sourcePW == nil || sourcePW.Product.Id == 0 { + return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id) + } + + sourceFlags := sourcePW.Product.Flags + if len(sourceFlags) == 0 { + s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id) + return nil + } + + hasDoc := false + hasPullet := false + hasLayer := false + for _, flag := range sourceFlags { + flagName := utils.FlagType(flag.Name) + if flagName == utils.FlagDOC { + hasDoc = true + } else if flagName == utils.FlagPullet { + hasPullet = true + } else if flagName == utils.FlagLayer { + hasLayer = true + } + } + + if hasDoc && hasPullet && hasLayer { + s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id) + _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyProjectFlockPopulation, + StockableID: population.Id, + ProductWarehouseID: sourcePW.Id, + Quantity: chickin.UsageQty, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err) + return err + } + return nil + } + + // LAYER only - no replenish needed + if hasLayer && !hasDoc && !hasPullet { + s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id) + return nil + } + + if hasDoc && !hasPullet && !hasLayer { + s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id) + _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyProjectFlockPopulation, + StockableID: population.Id, + ProductWarehouseID: targetPW.Id, + Quantity: chickin.UsageQty, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err) + return err + } + return nil + } + + if hasPullet && !hasDoc && !hasLayer { + s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id) + _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyProjectFlockPopulation, + StockableID: population.Id, + ProductWarehouseID: targetPW.Id, + Quantity: chickin.UsageQty, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err) + return err + } + return nil + } + + // Other combinations (e.g., DOC + PULLET without LAYER) - skip for now + s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id) + return nil +} + func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error { if chickin == nil || s.FifoSvc == nil { return nil @@ -703,8 +800,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, var currentUsage float64 if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil { - s.Log.Warnf("Failed to get current usage for chickin %d: %+v", chickin.Id, err) - currentUsage = 0 + } if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ @@ -716,14 +812,10 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, return err } - if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ - "usage_qty": 0, - "pending_usage_qty": 0, - }).Error; err != nil { + if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil { return err } - // Create stock log for the restoration if currentUsage > 0 { increaseLog := &entity.StockLog{ Increase: currentUsage, @@ -734,8 +826,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id), } if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil { - s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err) - // Don't return error here, stock already released + s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err) } } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 6176daeb..92ff2748 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -552,6 +552,7 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati } func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) { + availableQty := productWarehouse.Quantity if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { @@ -564,7 +565,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous } } - availableQty = productWarehouse.Quantity - totalPendingQty + totalPopulationQty := 0.0 + if s.PopulationRepo != nil { + popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id) + if err != nil { + s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err) + } else { + totalPopulationQty = popQty + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty if availableQty < 0 { availableQty = 0 } @@ -578,7 +589,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous } } - availableQty = productWarehouse.Quantity - totalPendingQty + totalPopulationQty := 0.0 + if s.PopulationRepo != nil { + popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id) + if err != nil { + s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err) + } else { + totalPopulationQty = popQty + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty if availableQty < 0 { availableQty = 0 } diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index fd263b27..04ae56e1 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -9,19 +9,17 @@ import ( ) type ProjectFlockPopulationRepository interface { - // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) - // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error - // transaction helpers WithTx(tx *gorm.DB) ProjectFlockPopulationRepository DB() *gorm.DB } @@ -108,6 +106,19 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI return total, nil } +func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Model(&entity.ProjectFlockPopulation{}). + Where("product_warehouse_id = ?", productWarehouseID). + Select("COALESCE(SUM(total_qty), 0)"). + Scan(&total).Error + if err != nil { + return 0, err + } + return total, nil +} + func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { var total float64 err := r.DB().WithContext(ctx). diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 42dcafd9..474a53c2 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface { ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) + GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository DB() *gorm.DB IdExists(ctx context.Context, id uint) (bool, error) @@ -206,6 +207,19 @@ func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uin return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id) } +func (r *projectFlockKandangRepositoryImpl) GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) { + var total float64 + err := r.db.WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(pending_usage_qty), 0)"). + Where("product_warehouse_id = ?", productWarehouseID). + Scan(&total).Error + if err != nil { + return 0, err + } + return total, nil +} + func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { record := new(entity.ProjectFlockKandang) if err := r.db.WithContext(ctx). diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index 27851b71..381f2492 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -2,6 +2,7 @@ package transfer_layings import ( "fmt" + "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -13,6 +14,7 @@ import ( rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" @@ -31,6 +33,44 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val productWarehouseRepo := rInventory.NewProductWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) + stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) + fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) + + + if err := fifoService.RegisterUsable(fifo.UsableConfig{ + Key: fifo.UsableKeyTransferToLaying, + Table: "laying_transfers", + Columns: fifo.UsableColumns{ + ID: "id", + ProductWarehouseID: "product_warehouse_id", + UsageQuantity: "usage_qty", + PendingQuantity: "pending_usage_qty", + CreatedAt: "created_at", + }, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err)) + } + } + + + if err := fifoService.RegisterStockable(fifo.StockableConfig{ + Key: fifo.StockableKeyTransferToLaying, + Table: "laying_transfers", + Columns: fifo.StockableColumns{ + ID: "id", + ProductWarehouseID: "dest_product_warehouse_id", + TotalQuantity: "total_qty", + TotalUsedQuantity: "total_used", + CreatedAt: "created_at", + }, + OrderBy: []string{"created_at ASC", "id ASC"}, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err)) + } + } + approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil { @@ -45,6 +85,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val productWarehouseRepo, warehouseRepo, approvalService, + fifoService, validate, ) userService := sUser.NewUserService(userRepo, validate) 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 0bfbc378..28fe9853 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -17,6 +17,7 @@ import ( repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" "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" @@ -45,6 +46,7 @@ type transferLayingService struct { ProductWarehouseRepo rInventory.ProductWarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository ApprovalService commonSvc.ApprovalService + FifoSvc commonSvc.FifoService } func NewTransferLayingService( @@ -55,6 +57,7 @@ func NewTransferLayingService( productWarehouseRepo rInventory.ProductWarehouseRepository, warehouseRepo rWarehouse.WarehouseRepository, approvalService commonSvc.ApprovalService, + fifoSvc commonSvc.FifoService, validate *validator.Validate, ) TransferLayingService { return &transferLayingService{ @@ -67,6 +70,7 @@ func NewTransferLayingService( ProductWarehouseRepo: productWarehouseRepo, WarehouseRepo: warehouseRepo, ApprovalService: approvalService, + FifoSvc: fifoSvc, } } @@ -268,15 +272,20 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) CreatedBy: actorID, } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + if len(sourceWarehouseMap) > 0 { + for _, pwID := range sourceWarehouseMap { + createBody.ProductWarehouseId = &pwID + break + } + } - if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + repoTx := s.Repository.WithTx(dbTransaction) + + if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record") } - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) - projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) - for _, sourceDetail := range req.SourceKandangs { productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] @@ -290,13 +299,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") } - if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population") - } - - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") - } } for _, targetDetail := range req.TargetKandangs { @@ -325,6 +327,22 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } } + // Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama) + if len(req.TargetKandangs) > 0 { + firstTargetPWID := req.TargetKandangs[0].ProjectFlockKandangId + // Cari ProductWarehouse untuk target kandang + targetWarehouse, _ := s.WarehouseRepo.GetLatestByKandangID(c.Context(), firstTargetPWID) + if targetWarehouse != nil { + // Query ProductWarehouse by warehouse and kandang + var targetPW entity.ProductWarehouse + err := dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, firstTargetPWID). + First(&targetPW).Error + if err == nil { + createBody.DestProductWarehouseID = &targetPW.Id + } + } + } + if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval") } @@ -339,7 +357,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return s.GetOne(c, createBody.Id) } -func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { +func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -381,6 +399,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + repoTx := s.Repository.WithTx(dbTransaction) projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) @@ -416,7 +435,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i totalSourceQty += source.Quantity } - if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), id, map[string]any{ + if err := repoTx.PatchOne(c.Context(), id, map[string]any{ "transfer_date": transferDate, "notes": req.Reason, "pending_usage_qty": &totalSourceQty, @@ -531,8 +550,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { } } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - + repoTx := s.Repository.WithTx(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) @@ -551,7 +571,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { } } - projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) for _, source := range sources { populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -575,7 +594,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { } } - if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil { + if err := repoTx.DeleteOne(c.Context(), id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") } @@ -624,14 +643,13 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - + repoTx := s.Repository.WithTx(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { - transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil) + transfer, err := repoTx.GetByID(c.Context(), approvableID, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID)) @@ -664,44 +682,45 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( } if len(sources) > 0 && len(targets) > 0 { - firstSource := sources[0] - if firstSource.ProductWarehouseId == nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) - } - sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse") - } - - for _, target := range targets { - - targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - continue - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + for _, source := range sources { + if source.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) } - targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + _, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ + UsableKey: fifo.UsableKeyTransferToLaying, + UsableID: approvableID, + ProductWarehouseID: *source.ProductWarehouseId, + Quantity: source.Qty, + AllowPending: false, + Tx: dbTransaction, + }) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - continue - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err)) + } + } + + if transfer.DestProductWarehouseID != nil { + note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) + replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyTransferToLaying, + StockableID: approvableID, + ProductWarehouseID: *transfer.DestProductWarehouseID, + Quantity: *transfer.PendingUsageQty, + Note: ¬e, + Tx: dbTransaction, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err)) } - if _, err := s.getOrCreateProductWarehouse( - c.Context(), - dbTransaction, - sourceWarehouse.ProductId, - targetWarehouse.Id, - target.Qty, - actorID, - &target.TargetProjectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock - ); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse") + if err := dbTransaction.Model(&entity.LayingTransfer{}). + Where("id = ?", approvableID). + Updates(map[string]interface{}{ + "total_qty": replenishResult.AddedQuantity, + }).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer") } } } @@ -709,9 +728,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( usageQty := *transfer.PendingUsageQty updateData := map[string]any{ "usage_qty": usageQty, + "total_qty": usageQty, // Same as usage_qty for initial transfer "pending_usage_qty": nil, } - if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil { + if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status") } } diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go index 7e80de38..fae714fb 100644 --- a/internal/modules/purchases/module.go +++ b/internal/modules/purchases/module.go @@ -75,7 +75,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) _ = fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKey("PURCHASE_ITEMS"), + Key: fifo.StockableKeyPurchaseItems, Table: "purchase_items", Columns: fifo.StockableColumns{ ID: "id", diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 68b21d6a..e46788d8 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -42,8 +42,7 @@ type PurchaseService interface { } const ( - priceTolerance = 0.0001 - purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS") + priceTolerance = 0.0001 ) type purchaseService struct { @@ -924,7 +923,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation continue } if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ - StockableKey: purchaseStockableKey, + StockableKey: fifo.StockableKeyPurchaseItems, StockableID: adj.itemID, ProductWarehouseID: adj.pwID, Quantity: adj.qty, diff --git a/internal/modules/repports/repositories/hpp_per_kandang.repository.go b/internal/modules/repports/repositories/hpp_per_kandang.repository.go index 7e1c8143..6d4185e8 100644 --- a/internal/modules/repports/repositories/hpp_per_kandang.repository.go +++ b/internal/modules/repports/repositories/hpp_per_kandang.repository.go @@ -106,7 +106,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Where("r.deleted_at IS NULL") recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) - purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String() + purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String() query := r.db.WithContext(ctx). diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index ea6f96c0..2f96beaa 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -1,7 +1,18 @@ package fifo const ( - UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" - UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" - UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" + // Usable Keys + UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" + UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" + UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" + UsableKeyTransferToLaying UsableKey = "TRANSFER_TO_LAYING" + UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" + UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" + + // Stockable Keys + StockableKeyTransferToLaying StockableKey = "TRANSFER_TO_LAYING" + StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" + StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" + StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" + StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" )