FIX[BE]: fixing bug transfer to laying, delet biaya, nominal expesen e, chickin

This commit is contained in:
aguhh18
2026-01-07 09:27:39 +07:00
parent a08466a28e
commit 0a84e427c1
21 changed files with 432 additions and 126 deletions
@@ -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;
@@ -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);
@@ -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;
@@ -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';
+17 -6
View File
@@ -12,18 +12,29 @@ type LayingTransfer struct {
FromProjectFlockId uint `gorm:"not null"` FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"` TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"` PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *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"` Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"`
} }
@@ -36,7 +36,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
err := fifoService.RegisterStockable(fifo.StockableConfig{ err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKey("ADJUSTMENT_IN"), Key: fifo.StockableKeyAdjustmentIn,
Table: "adjustment_stocks", Table: "adjustment_stocks",
Columns: fifo.StockableColumns{ Columns: fifo.StockableColumns{
ID: "id", ID: "id",
@@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
} }
err = fifoService.RegisterUsable(fifo.UsableConfig{ err = fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKey("ADJUSTMENT_OUT"), Key: fifo.UsableKeyAdjustmentOut,
Table: "adjustment_stocks", Table: "adjustment_stocks",
Columns: fifo.UsableColumns{ Columns: fifo.UsableColumns{
ID: "id", ID: "id",
@@ -20,6 +20,7 @@ import (
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
projectFlockKandangID = &pfkID projectFlockKandangID = &pfkID
} }
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk( pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
ctx,
uint(req.ProductID),
uint(req.WarehouseID),
projectFlockKandangID,
)
if err != nil { if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) { 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") 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 { 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
} }
pw = newPW 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") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
// Create StockLog for history tracking
afterQuantity := productWarehouse.Quantity afterQuantity := productWarehouse.Quantity
newLog := &entity.StockLog{ newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment), LoggableType: string(utils.StockLogTypeAdjustment),
@@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return err return err
} }
// Create AdjustmentStock record for FIFO tracking
adjustmentStock := &entity.AdjustmentStock{ adjustmentStock := &entity.AdjustmentStock{
StockLogId: newLog.Id, StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
@@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
// Adjustment INCREASE → Replenish stock (Stockable)
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{ _, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
StockableKey: "ADJUSTMENT_IN", StockableKey: fifo.StockableKeyAdjustmentIn,
StockableID: adjustmentStock.Id, StockableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, Quantity: req.Quantity,
@@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
} else { } else {
// Adjustment DECREASE → Consume stock (Usable)
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{ _, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
UsableKey: "ADJUSTMENT_OUT", UsableKey: fifo.UsableKeyAdjustmentOut,
UsableID: adjustmentStock.Id, UsableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, 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) // Update ProductWarehouse quantity (for backward compatibility/reporting)
productWarehouse.Quantity = afterQuantity productWarehouse.Quantity = afterQuantity
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { 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) s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
@@ -43,12 +43,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
panic(err) panic(err)
} }
// Initialize FIFO Service
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
// Register Transfer as Stockable (adds stock to destination warehouse)
err = fifoService.RegisterStockable(fifo.StockableConfig{ err = fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKey("STOCK_TRANSFER_IN"), Key: fifo.StockableKeyStockTransferIn,
Table: "stock_transfer_details", Table: "stock_transfer_details",
Columns: fifo.StockableColumns{ Columns: fifo.StockableColumns{
ID: "id", ID: "id",
@@ -63,9 +61,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
panic(err) panic(err)
} }
// Register Transfer as Usable (consumes stock from source warehouse)
err = fifoService.RegisterUsable(fifo.UsableConfig{ err = fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKey("STOCK_TRANSFER_OUT"), Key: fifo.UsableKeyStockTransferOut,
Table: "stock_transfer_details", Table: "stock_transfer_details",
Columns: fifo.UsableColumns{ Columns: fifo.UsableColumns{
ID: "id", ID: "id",
@@ -21,6 +21,7 @@ import (
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "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 { for _, product := range req.Products {
detail := detailMap[uint64(product.ProductID)] detail := detailMap[uint64(product.ProductID)]
// Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT)
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: "STOCK_TRANSFER_OUT", UsableKey: fifo.UsableKeyStockTransferOut,
UsableID: uint(detail.Id), UsableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.SourceProductWarehouseID), ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Quantity: product.ProductQty, Quantity: product.ProductQty,
AllowPending: false, // Don't allow pending, must have actual stock AllowPending: false,
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err)) 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{}). if err := tx.Model(&entity.StockTransferDetail{}).
Where("id = ?", detail.Id). Where("id = ?", detail.Id).
Updates(map[string]interface{}{ 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) // Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: "STOCK_TRANSFER_IN", StockableKey: fifo.StockableKeyStockTransferIn,
StockableID: uint(detail.Id), StockableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.DestProductWarehouseID), ProductWarehouseID: uint(*detail.DestProductWarehouseID),
Quantity: product.ProductQty, 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)) 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{}). if err := tx.Model(&entity.StockTransferDetail{}).
Where("id = ?", detail.Id). Where("id = ?", detail.Id).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
@@ -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) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil { if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
@@ -17,6 +17,7 @@ type ProjectChickinRepository interface {
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, 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 { type ChickinRepositoryImpl struct {
@@ -123,3 +124,13 @@ func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.C
Scan(&result).Error Scan(&result).Error
return result, err 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
}
@@ -214,9 +214,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
pendingQtyMap[existingChickin.ProductWarehouseId] += existingChickin.PendingUsageQty 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 { for idx, chickin := range newChikins {
pendingQty := pendingQtyMap[chickin.ProductWarehouseId] pendingQty := pendingQtyMap[chickin.ProductWarehouseId]
desiredQty := chickinQtyMap[uint(idx)] desiredQty := chickinQtyMap[uint(idx)]
@@ -232,8 +229,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
chickinQtyMap[uint(idx)] = availableQty 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 pendingQtyMap[chickin.ProductWarehouseId] += availableQty
} }
@@ -358,12 +353,15 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
if chickin.UsageQty > 0 { if chickin.UsageQty > 0 {
currentUsageQty := chickin.UsageQty
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil { if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
return err return err
} }
warehouseDeltas := make(map[uint]float64) 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 { 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) s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
return err return err
@@ -618,7 +616,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
population := &entity.ProjectFlockPopulation{ population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id, ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id, ProductWarehouseId: targetPW.Id,
TotalQty: quantityToConvert, TotalQty: 0, // Will be set by FIFO Replenish
TotalUsedQty: 0, TotalUsedQty: 0,
Notes: chickin.Notes, Notes: chickin.Notes,
CreatedBy: actorID, 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) 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 totalQuantityAdded += quantityToConvert
} }
// NOTE: Tidak menambah target ProductWarehouse quantity karena: // NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
// 1. Ayam sudah dipakai (masuk population) // yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
// 2. ProductWarehouse source sudah berkurang saat create chickin (ConsumeChickinStocks) // - DOC → replenish ke PULLET
// 3. Menambah quantity disini akan menyebabkan double count // - PULLET → replenish ke LAYER
// // - LAYER → tidak perlu replenish (sudah final)
// PULLET/LAYER untuk flock ini akan di-add lewat mekanisme lain (misal: purchase, transfer, dll) // - DOC+PULLET+LAYER → replenish ke dirinya sendiri
return nil 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", s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations)) result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
"usage_qty": result.UsageQuantity,
"pending_usage_qty": result.PendingQuantity,
}).Error; err != nil {
return err return err
} }
@@ -696,6 +698,101 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
return nil 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 { func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
if chickin == nil || s.FifoSvc == nil { if chickin == nil || s.FifoSvc == nil {
return nil return nil
@@ -703,8 +800,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
var currentUsage float64 var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil { if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).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{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
@@ -716,14 +812,10 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
return err return err
} }
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
"usage_qty": 0,
"pending_usage_qty": 0,
}).Error; err != nil {
return err return err
} }
// Create stock log for the restoration
if currentUsage > 0 { if currentUsage > 0 {
increaseLog := &entity.StockLog{ increaseLog := &entity.StockLog{
Increase: currentUsage, 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), Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
} }
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil { 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) s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
// Don't return error here, stock already released
} }
} }
@@ -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) { func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
availableQty := productWarehouse.Quantity availableQty := productWarehouse.Quantity
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { 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 { if availableQty < 0 {
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 { if availableQty < 0 {
availableQty = 0 availableQty = 0
} }
@@ -9,19 +9,17 @@ import (
) )
type ProjectFlockPopulationRepository interface { type ProjectFlockPopulationRepository interface {
// domain-specific
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID 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) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID 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 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 PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
// transaction helpers
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
DB() *gorm.DB DB() *gorm.DB
} }
@@ -108,6 +106,19 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
return total, nil 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) { func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64 var total float64
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
IdExists(ctx context.Context, id uint) (bool, error) 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) 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) { func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang) record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
@@ -2,6 +2,7 @@ package transfer_layings
import ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -13,6 +14,7 @@ import (
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" 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" 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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" 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" 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) productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(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) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil { 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, productWarehouseRepo,
warehouseRepo, warehouseRepo,
approvalService, approvalService,
fifoService,
validate, validate,
) )
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -17,6 +17,7 @@ import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" 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" 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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -45,6 +46,7 @@ type transferLayingService struct {
ProductWarehouseRepo rInventory.ProductWarehouseRepository ProductWarehouseRepo rInventory.ProductWarehouseRepository
WarehouseRepo rWarehouse.WarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository
ApprovalService commonSvc.ApprovalService ApprovalService commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
} }
func NewTransferLayingService( func NewTransferLayingService(
@@ -55,6 +57,7 @@ func NewTransferLayingService(
productWarehouseRepo rInventory.ProductWarehouseRepository, productWarehouseRepo rInventory.ProductWarehouseRepository,
warehouseRepo rWarehouse.WarehouseRepository, warehouseRepo rWarehouse.WarehouseRepository,
approvalService commonSvc.ApprovalService, approvalService commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate, validate *validator.Validate,
) TransferLayingService { ) TransferLayingService {
return &transferLayingService{ return &transferLayingService{
@@ -67,6 +70,7 @@ func NewTransferLayingService(
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ApprovalService: approvalService, ApprovalService: approvalService,
FifoSvc: fifoSvc,
} }
} }
@@ -268,15 +272,20 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
CreatedBy: actorID, 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") 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 { for _, sourceDetail := range req.SourceKandangs {
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] 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") 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 { 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 { if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval") 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) 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 { if err := s.Validate.Struct(req); err != nil {
return nil, err 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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.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 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, "transfer_date": transferDate,
"notes": req.Reason, "notes": req.Reason,
"pending_usage_qty": &totalSourceQty, "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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) 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 { for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 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") 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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs { 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID)) 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 { 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) for _, source := range sources {
if err != nil { if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse") return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
}
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")
} }
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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
continue }
} }
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
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: &note,
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( if err := dbTransaction.Model(&entity.LayingTransfer{}).
c.Context(), Where("id = ?", approvableID).
dbTransaction, Updates(map[string]interface{}{
sourceWarehouse.ProductId, "total_qty": replenishResult.AddedQuantity,
targetWarehouse.Id, }).Error; err != nil {
target.Qty, return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
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")
} }
} }
} }
@@ -709,9 +728,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
usageQty := *transfer.PendingUsageQty usageQty := *transfer.PendingUsageQty
updateData := map[string]any{ updateData := map[string]any{
"usage_qty": usageQty, "usage_qty": usageQty,
"total_qty": usageQty, // Same as usage_qty for initial transfer
"pending_usage_qty": nil, "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") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
_ = fifoService.RegisterStockable(fifo.StockableConfig{ _ = fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKey("PURCHASE_ITEMS"), Key: fifo.StockableKeyPurchaseItems,
Table: "purchase_items", Table: "purchase_items",
Columns: fifo.StockableColumns{ Columns: fifo.StockableColumns{
ID: "id", ID: "id",
@@ -42,8 +42,7 @@ type PurchaseService interface {
} }
const ( const (
priceTolerance = 0.0001 priceTolerance = 0.0001
purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS")
) )
type purchaseService struct { type purchaseService struct {
@@ -924,7 +923,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
continue continue
} }
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: purchaseStockableKey, StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID, StockableID: adj.itemID,
ProductWarehouseID: adj.pwID, ProductWarehouseID: adj.pwID,
Quantity: adj.qty, Quantity: adj.qty,
@@ -106,7 +106,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Where("r.deleted_at IS NULL") Where("r.deleted_at IS NULL")
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String() purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String() transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
+14 -3
View File
@@ -1,7 +1,18 @@
package fifo package fifo
const ( const (
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" // Usable Keys
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" 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"
) )