Merge branch 'FIX/BE/Transfer_to_laying' into 'development'

[FIX][BE]: fixing transfer to laying qty doesn't  listed on product warehouse and fixing wrong implementation of fifo stock on laying transfer

See merge request mbugroup/lti-api!151
This commit is contained in:
Hafizh A. Y.
2026-01-12 03:38:27 +00:00
11 changed files with 422 additions and 354 deletions
@@ -0,0 +1,79 @@
-- Rollback: Revert FIFO fields back to laying_transfers from detail tables
-- ============================================================================
-- PART 1: Remove FIFO columns from detail tables
-- ============================================================================
-- Add back old qty column first
ALTER TABLE laying_transfer_sources
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE laying_transfer_targets
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
-- Now drop FIFO columns
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_usage_qty;
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add back FIFO columns to laying_transfers table
-- ============================================================================
-- Add columns back for USABLE role (source warehouse)
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT,
ADD COLUMN pending_usage_qty NUMERIC(15, 3),
ADD COLUMN usage_qty NUMERIC(15, 3);
-- Add columns back 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 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- ============================================================================
-- PART 3: Recreate foreign key constraints
-- ============================================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
-- Add source product warehouse FK
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;
-- Add destination product warehouse FK
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 $$;
-- ============================================================================
-- PART 4: Recreate indexes for performance
-- ============================================================================
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- ============================================================================
-- PART 5: Recreate comments 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 FIFO STOCKABLE role';
@@ -0,0 +1,73 @@
-- Move FIFO fields from laying_transfers to detail tables (sources & targets)
-- This enables proper FIFO integration for transfer laying with multiple sources and targets
-- ============================================================================
-- PART 1: Remove FIFO-related columns from laying_transfers table
-- ============================================================================
-- Drop foreign key constraints first
DO $$
BEGIN
-- Drop source product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_product_warehouse_id;
END IF;
-- Drop destination product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id;
END IF;
END $$;
-- Drop indexes
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Remove columns from laying_transfers
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id,
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS pending_usage_qty,
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_sources
ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role';
COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role';
-- Drop old qty column as it's replaced by usage_qty
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS qty;
-- ============================================================================
-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_targets
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role';
COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role';
-- Drop old qty column as it's replaced by total_qty
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS qty;
-13
View File
@@ -12,17 +12,6 @@ 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)"`
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"`
@@ -31,8 +20,6 @@ type LayingTransfer struct {
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"`
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"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
+2 -1
View File
@@ -11,7 +11,8 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"` SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -1
View File
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"` TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"` TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -96,9 +96,9 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
var total float64 var total float64
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Table("project_flock_populations"). Table("project_flock_populations").
Select("COALESCE(SUM(total_qty), 0) AS total_qty"). Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS available_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). Joins("JOIN product_warehouses pw ON project_flock_populations.product_warehouse_id = pw.id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
return 0, err return 0, err
@@ -84,7 +84,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create) req := new(validation.Create)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Format permintaan tidak valid")
} }
result, err := u.TransferLayingService.CreateOne(c, req) result, err := u.TransferLayingService.CreateOne(c, req)
@@ -96,7 +96,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusCreated, Code: fiber.StatusCreated,
Status: "success", Status: "success",
Message: "Create transferLaying successfully", Message: "Berhasil membuat transfer laying",
Data: dto.ToTransferLayingListDTO(*result), Data: dto.ToTransferLayingListDTO(*result),
}) })
} }
@@ -67,8 +67,6 @@ type TransferLayingListDTO struct {
TransferLayingRelationDTO TransferLayingRelationDTO
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
PendingUsageQty *float64 `json:"pending_usage_qty"`
UsageQty *float64 `json:"usage_qty"`
CreatedBy uint `json:"created_by"` CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -166,7 +164,7 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
return LayingTransferSourceDTO{ return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.Qty, Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note, Note: source.Note,
} }
@@ -186,7 +184,7 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
return LayingTransferTargetDTO{ return LayingTransferTargetDTO{
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.Qty, Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity)
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note, Note: target.Note,
} }
@@ -223,8 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), TransferLayingRelationDTO: ToTransferLayingRelationDTO(e),
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy, CreatedBy: e.CreatedBy,
CreatedUser: createdUser, CreatedUser: createdUser,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
@@ -26,6 +26,8 @@ type TransferLayingModule struct{}
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
layingTransferSourceRepo := rTransferLaying.NewLayingTransferSourceRepository(db)
layingTransferTargetRepo := rTransferLaying.NewLayingTransferTargetRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
@@ -36,30 +38,13 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
// daftarin jadi stockable
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{ if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyTransferToLaying, Key: fifo.StockableKeyTransferToLayingIn,
Table: "laying_transfers", Table: "laying_transfer_targets",
Columns: fifo.StockableColumns{ Columns: fifo.StockableColumns{
ID: "id", ID: "id",
ProductWarehouseID: "dest_product_warehouse_id", ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty", TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used", TotalUsedQuantity: "total_used",
CreatedAt: "created_at", CreatedAt: "created_at",
@@ -71,6 +56,24 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
} }
} }
// daftarin jadi usable
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyTransferToLayingOut,
Table: "laying_transfer_sources",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_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 transfer to laying usable 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 {
@@ -79,6 +82,8 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
transferLayingService := sTransferLaying.NewTransferLayingService( transferLayingService := sTransferLaying.NewTransferLayingService(
transferLayingRepo, transferLayingRepo,
layingTransferSourceRepo,
layingTransferTargetRepo,
projectFlockRepo, projectFlockRepo,
projectFlockKandangRepo, projectFlockKandangRepo,
projectFlockPopulationRepo, projectFlockPopulationRepo,
@@ -16,6 +16,7 @@ import (
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
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"
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" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
@@ -40,17 +41,22 @@ type transferLayingService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.TransferLayingRepository Repository repository.TransferLayingRepository
LayingTransferSourceRepo repository.LayingTransferSourceRepository
LayingTransferTargetRepo repository.LayingTransferTargetRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
ProductWarehouseRepo rInventory.ProductWarehouseRepository ProductWarehouseRepo rInventory.ProductWarehouseRepository
WarehouseRepo rWarehouse.WarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository
StockLogRepo rStockLogs.StockLogRepository
ApprovalService commonSvc.ApprovalService ApprovalService commonSvc.ApprovalService
FifoSvc commonSvc.FifoService FifoSvc commonSvc.FifoService
} }
func NewTransferLayingService( func NewTransferLayingService(
repo repository.TransferLayingRepository, repo repository.TransferLayingRepository,
layingTransferSourceRepo repository.LayingTransferSourceRepository,
layingTransferTargetRepo repository.LayingTransferTargetRepository,
projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
@@ -64,11 +70,14 @@ func NewTransferLayingService(
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
LayingTransferSourceRepo: layingTransferSourceRepo,
LayingTransferTargetRepo: layingTransferTargetRepo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockPopulationRepo: projectFlockPopulationRepo, ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
ApprovalService: approvalService, ApprovalService: approvalService,
FifoSvc: fifoSvc, FifoSvc: fifoSvc,
} }
@@ -164,55 +173,42 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err return nil, err
} }
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil { if err := commonSvc.EnsureRelations(c.Context(),
if errors.Is(err, gorm.ErrRecordNotFound) { commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found") commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
} ); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock") return nil, err
} }
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil { sourceKandangIDs := make([]uint, len(req.SourceKandangs))
if errors.Is(err, gorm.ErrRecordNotFound) { for i, detail := range req.SourceKandangs {
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found") sourceKandangIDs[i] = detail.ProjectFlockKandangId
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
} }
for _, detail := range req.SourceKandangs { if err := s.validateKandangOwnership(
if err := commonSvc.EnsureRelations(c.Context(), c.Context(),
commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, req.SourceProjectFlockId,
); err != nil { sourceKandangIDs,
return nil, err ); err != nil {
} return nil, err
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang")
}
if pfk.ProjectFlockId != req.SourceProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId))
}
} }
for _, detail := range req.TargetKandangs { targetKandangIDs := make([]uint, len(req.TargetKandangs))
if err := commonSvc.EnsureRelations(c.Context(), for i, detail := range req.TargetKandangs {
commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, targetKandangIDs[i] = detail.ProjectFlockKandangId
); err != nil { }
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) if err := s.validateKandangOwnership(
if err != nil { c.Context(),
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") req.TargetProjectFlockId,
} targetKandangIDs,
if pfk.ProjectFlockId != req.TargetProjectFlockId { ); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId)) return nil, err
}
} }
transferDate, err := utils.ParseDateString(req.TransferDate) transferDate, err := utils.ParseDateString(req.TransferDate)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Format tanggal transfer tidak valid")
} }
var totalSourceQty, totalTargetQty float64 var totalSourceQty, totalTargetQty float64
@@ -220,7 +216,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 { if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0") return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
} }
totalSourceQty += sourceDetail.Quantity totalSourceQty += sourceDetail.Quantity
@@ -239,11 +235,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
if totalPopulation == 0 { if totalPopulation == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId))
} }
if totalPopulation < sourceDetail.Quantity { if totalPopulation < sourceDetail.Quantity {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
} }
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
@@ -251,13 +247,13 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 { if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0") return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
} }
totalTargetQty += targetDetail.Quantity totalTargetQty += targetDetail.Quantity
} }
if totalSourceQty != totalTargetQty { if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
} }
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano()) transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
@@ -268,22 +264,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
FromProjectFlockId: req.SourceProjectFlockId, FromProjectFlockId: req.SourceProjectFlockId,
ToProjectFlockId: req.TargetProjectFlockId, ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate, TransferDate: transferDate,
PendingUsageQty: &totalSourceQty,
CreatedBy: actorID, CreatedBy: actorID,
} }
if len(sourceWarehouseMap) > 0 {
for _, pwID := range sourceWarehouseMap {
createBody.ProductWarehouseId = &pwID
break
}
}
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) repoTx := s.Repository.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
pwRepoTx := rInventory.NewProductWarehouseRepository(dbTransaction)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil { 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, "Gagal membuat record transfer laying")
} }
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
@@ -292,78 +284,88 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: createBody.Id, LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
Qty: sourceDetail.Quantity, UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
} }
if err := dbTransaction.Create(&source).Error; err != nil { if err := sourceRepoTx.CreateOne(c.Context(), &source, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat sumber transfer")
} }
} }
var firstTargetProductWarehouseID *uint for _, targetDetail := range req.TargetKandangs {
for i, targetDetail := range req.TargetKandangs { targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan project flock kandang tujuan")
} }
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse tidak ditemukan untuk kandang tujuan %d", targetDetail.ProjectFlockKandangId))
} }
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse tujuan")
} }
var targetPW entity.ProductWarehouse // Ambil product ID dari salah satu source warehouse (harusnya semua sources product-nya sama)
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId). var sourceProductID uint
First(&targetPW).Error for _, sourceDetail := range req.SourceKandangs {
if pwID, ok := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]; ok {
// Get product warehouse untuk ambil product ID
sourcePW, err := pwRepoTx.GetByID(c.Context(), pwID, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
break
}
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan product dari source warehouse")
}
// Cari product warehouse di target berdasarkan: warehouse + project_flock_kandang + PRODUCT
targetPW, err := pwRepoTx.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id)) newTargetPW := entity.ProductWarehouse{
ProductId: sourceProductID,
WarehouseId: targetWarehouse.Id,
ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
Quantity: 0,
}
if err := pwRepoTx.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
}
targetPW = &newTargetPW
} else {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mendapatkan product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err))
} }
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
} }
target := entity.LayingTransferTarget{ target := entity.LayingTransferTarget{
LayingTransferId: createBody.Id, LayingTransferId: createBody.Id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
Qty: targetDetail.Quantity, TotalQty: targetDetail.Quantity,
TotalUsed: 0,
ProductWarehouseId: &targetPW.Id, ProductWarehouseId: &targetPW.Id,
} }
if err := dbTransaction.Create(&target).Error; err != nil { if err := targetRepoTx.CreateOne(c.Context(), &target, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat target transfer")
}
if i == 0 {
firstTargetProductWarehouseID = &targetPW.Id
}
}
// Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama)
if firstTargetProductWarehouseID != nil {
createBody.DestProductWarehouseID = firstTargetProductWarehouseID
// Update DestProductWarehouseID ke database
if err := dbTransaction.Model(&entity.LayingTransfer{}).
Where("id = ?", createBody.Id).
Update("dest_product_warehouse_id", *firstTargetProductWarehouseID).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update DestProductWarehouseID")
} }
} }
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, "Gagal membuat approval transfer")
} }
return nil return nil
}) })
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying") return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying")
} }
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
@@ -412,53 +414,32 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
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) repoTx := s.Repository.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
// Hapus old sources dan targets
for _, oldSource := range existingTransfer.Sources { for _, oldSource := range existingTransfer.Sources {
if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 { if err := sourceRepo.DeleteOne(c.Context(), oldSource.Id); err != nil {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{
"qty": gorm.Expr("qty + ?", oldSource.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity")
}
if err := s.restoreProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, oldSource.SourceProjectFlockKandangId, oldSource.Qty); err != nil {
return err
}
}
}
for _, oldSource := range existingTransfer.Sources {
if err := dbTransaction.Delete(&oldSource).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source")
} }
} }
for _, oldTarget := range existingTransfer.Targets { for _, oldTarget := range existingTransfer.Targets {
if err := dbTransaction.Delete(&oldTarget).Error; err != nil { if err := targetRepo.DeleteOne(c.Context(), oldTarget.Id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target")
} }
} }
totalSourceQty := 0.0
for _, source := range req.SourceKandangs {
totalSourceQty += source.Quantity
}
if err := repoTx.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,
}, nil); err != nil { }, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header")
} }
sourceWarehouseMap := make(map[uint]uint) // Create new sources dengan pending quantity
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations")
} }
@@ -467,48 +448,39 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId))
} }
var totalPopulation float64
var productWarehouseId uint var productWarehouseId uint
for _, pop := range populations { for _, pop := range populations {
totalPopulation += pop.TotalQty
if pop.ProductWarehouseId > 0 { if pop.ProductWarehouseId > 0 {
productWarehouseId = pop.ProductWarehouseId productWarehouseId = pop.ProductWarehouseId
break
} }
} }
if totalPopulation < sourceDetail.Quantity { if productWarehouseId == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId))
} }
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: id, LayingTransferId: id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
Qty: sourceDetail.Quantity, UsageQty: 0,
PendingUsageQty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
} }
if err := dbTransaction.Create(&source).Error; err != nil { if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
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 err
}
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")
}
} }
pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
} }
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
@@ -516,23 +488,50 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
} }
var targetPW entity.ProductWarehouse // Ambil product ID dari source yang pertama (semua sources seharusnya product-nya sama)
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId). var sourceProductID uint
First(&targetPW).Error if len(req.SourceKandangs) > 0 {
firstSourceKandangID := req.SourceKandangs[0].ProjectFlockKandangId
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), firstSourceKandangID)
if err == nil && len(populations) > 0 && populations[0].ProductWarehouseId > 0 {
sourcePW, err := pwRepo.GetByID(c.Context(), populations[0].ProductWarehouseId, nil)
if err == nil {
sourceProductID = sourcePW.ProductId
}
}
}
if sourceProductID == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse")
}
targetPW, err := pwRepo.FindByProductWarehouseAndPfk(c.Context(), sourceProductID, targetWarehouse.Id, &targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
newTargetPW := entity.ProductWarehouse{
ProductId: sourceProductID,
WarehouseId: targetWarehouse.Id,
ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId,
Quantity: 0,
}
if err := pwRepo.CreateOne(c.Context(), &newTargetPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
}
targetPW = &newTargetPW
} else {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
} }
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
} }
target := entity.LayingTransferTarget{ target := entity.LayingTransferTarget{
LayingTransferId: id, LayingTransferId: id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
Qty: targetDetail.Quantity, TotalQty: targetDetail.Quantity,
TotalUsed: 0,
ProductWarehouseId: &targetPW.Id, ProductWarehouseId: &targetPW.Id,
} }
if err := dbTransaction.Create(&target).Error; err != nil { if err := targetRepo.CreateOne(c.Context(), &target, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
} }
} }
@@ -560,6 +559,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
@@ -573,48 +573,6 @@ 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) 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)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
for _, source := range sources {
if source.ProductWarehouseId != nil && source.Qty > 0 {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
"qty": gorm.Expr("qty + ?", source.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
}
}
}
for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
}
remainingToRestore := source.Qty
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
pop := populations[i]
restoreAmount := remainingToRestore
if pop.TotalQty < remainingToRestore {
restoreAmount = pop.TotalQty
}
newQty := pop.TotalQty + restoreAmount
if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity")
}
remainingToRestore -= restoreAmount
}
}
if err := repoTx.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")
@@ -667,6 +625,8 @@ 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) repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
// Gunakan repo baru untuk transaction scope agar bisa akses method custom
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
@@ -691,70 +651,73 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
} }
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 { if action == entity.ApprovalActionApproved {
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer")
} }
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID) targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer")
} }
if len(sources) > 0 && len(targets) > 0 { // Hitung total quantity dari targets untuk di-consume dari sources
totalTargetQty := 0.0
for _, target := range targets {
totalTargetQty += target.TotalQty
}
for _, source := range sources { // Consume dari laying_transfer_sources (Usable) - akan consume dari ProjectFlockPopulation (Stockable)
if source.ProductWarehouseId == nil { for _, source := range sources {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) if source.ProductWarehouseId == nil {
} return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
_, 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 {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
}
} }
if transfer.DestProductWarehouseID != nil { consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) UsableKey: fifo.UsableKeyTransferToLayingOut,
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ UsableID: source.Id,
StockableKey: fifo.StockableKeyTransferToLaying, ProductWarehouseID: *source.ProductWarehouseId,
StockableID: approvableID, Quantity: totalTargetQty,
ProductWarehouseID: *transfer.DestProductWarehouseID, AllowPending: false,
Quantity: *transfer.PendingUsageQty, Tx: dbTransaction,
Note: &note, })
Tx: dbTransaction, if err != nil {
}) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
if err != nil { }
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err))
}
if err := dbTransaction.Model(&entity.LayingTransfer{}). if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{
Where("id = ?", approvableID). "usage_qty": source.UsageQty + consumeResult.UsageQuantity,
Updates(map[string]interface{}{ "pending_usage_qty": consumeResult.PendingQuantity,
"total_qty": replenishResult.AddedQuantity, }, nil); err != nil {
}).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
}
} }
} }
usageQty := *transfer.PendingUsageQty for _, target := range targets {
updateData := map[string]any{ if target.ProductWarehouseId == nil {
"usage_qty": usageQty, return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
"total_qty": usageQty, // Same as usage_qty for initial transfer }
"pending_usage_qty": nil,
} note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil { replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status") StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id,
ProductWarehouseID: *target.ProductWarehouseId,
Quantity: target.TotalQty,
Note: &note,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
}
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": replenishResult.AddedQuantity,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
}
} }
} }
} }
@@ -820,9 +783,8 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
newWarehouse := &entity.ProductWarehouse{ newWarehouse := &entity.ProductWarehouse{
ProductId: productID, ProductId: productID,
WarehouseId: warehouseID, WarehouseId: warehouseID,
ProjectFlockKandangId: projectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock ProjectFlockKandangId: projectFlockKandangId,
Quantity: quantity, Quantity: quantity,
// CreatedBy: actorID,
} }
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
@@ -832,66 +794,6 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
return newWarehouse, nil return newWarehouse, nil
} }
func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction")
}
remainingToReduce := quantityToReduce
for i := len(populations) - 1; i >= 0; i-- {
if remainingToReduce <= 0 {
break
}
pop := populations[i]
reductionAmount := remainingToReduce
if pop.TotalQty < remainingToReduce {
reductionAmount = pop.TotalQty
}
newQty := pop.TotalQty - reductionAmount
if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
remainingToReduce -= reductionAmount
}
if remainingToReduce > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce))
}
return nil
}
func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToRestore float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration")
}
if len(populations) > 0 {
lastPop := populations[len(populations)-1]
newQty := lastPop.TotalQty + quantityToRestore
if err := populationRepo.PatchOne(ctx, lastPop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
}
return nil
}
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
@@ -925,3 +827,27 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
return pf, kandangAvailableQty, nil return pf, kandangAvailableQty, nil
} }
func (s *transferLayingService) validateKandangOwnership(
ctx context.Context,
projectFlockID uint,
kandangIDs []uint,
) error {
for _, kandangID := range kandangIDs {
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(ctx, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang %d tidak ditemukan", kandangID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get project flock kandang")
}
if projectFlockKandang.ProjectFlockId != projectFlockID {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak terhubung ke project flock %d", kandangID, projectFlockID))
}
}
return nil
}
+10 -10
View File
@@ -2,17 +2,17 @@ package fifo
const ( const (
// Usable Keys // Usable Keys
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
UsableKeyTransferToLaying UsableKey = "TRANSFER_TO_LAYING" UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
// Stockable Keys // Stockable Keys
StockableKeyTransferToLaying StockableKey = "TRANSFER_TO_LAYING" StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
) )