From 8cd9627a511bfcc7f5d935d5cb149da28bd65f04 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 14:34:08 +0700 Subject: [PATCH] feat[BE]: Add requested_qty field to LayingTransferSource and update related logic for transfer operations --- ...ed_qty_to_laying_transfer_sources.down.sql | 4 ++ ...sted_qty_to_laying_transfer_sources.up.sql | 9 +++ internal/entities/laying_transfer_source.go | 1 + internal/modules/inventory/transfers/route.go | 6 +- .../chickins/services/chickin.service.go | 4 +- .../project_flock_population_repository.go | 23 +++++++- .../dto/transfer_laying.dto.go | 12 +++- .../services/transfer_laying.service.go | 58 ++++++++++++++++--- .../validations/transfer_laying.validation.go | 12 ++-- 9 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql create mode 100644 internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql diff --git a/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql new file mode 100644 index 00000000..7dd06499 --- /dev/null +++ b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql @@ -0,0 +1,4 @@ +-- Rollback: Remove requested_qty column from laying_transfer_sources table + +ALTER TABLE laying_transfer_sources +DROP COLUMN IF EXISTS requested_qty; diff --git a/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql new file mode 100644 index 00000000..dc28ca74 --- /dev/null +++ b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql @@ -0,0 +1,9 @@ +-- Add requested_qty column to laying_transfer_sources table +-- This field stores the quantity requested by user during create/update +-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending) + +ALTER TABLE laying_transfer_sources +ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update'; diff --git a/internal/entities/laying_transfer_source.go b/internal/entities/laying_transfer_source.go index e0b85774..b284746d 100644 --- a/internal/entities/laying_transfer_source.go +++ b/internal/entities/laying_transfer_source.go @@ -11,6 +11,7 @@ type LayingTransferSource struct { LayingTransferId uint `gorm:"index;not null"` SourceProjectFlockKandangId uint `gorm:"not null"` ProductWarehouseId *uint `gorm:""` + RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user 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"` diff --git a/internal/modules/inventory/transfers/route.go b/internal/modules/inventory/transfers/route.go index d24dbcb4..f754148c 100644 --- a/internal/modules/inventory/transfers/route.go +++ b/internal/modules/inventory/transfers/route.go @@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ route := v1.Group("/transfers") route.Use(m.Auth(u)) - route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) - route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) + route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 84e98f2d..b39dca78 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti newChikins = append(newChikins, newChickin) - totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) + totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId)) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } availableQty := productWarehouse.Quantity - totalPopulationQty diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index 022da6a3..36fe8cbc 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "math" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface { GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, 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 @@ -111,7 +113,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c err := r.DB().WithContext(ctx). Model(&entity.ProjectFlockPopulation{}). Where("product_warehouse_id = ?", productWarehouseID). - Select("COALESCE(SUM(total_qty), 0)"). + Select("COALESCE(SUM(total_qty - total_used_qty), 0)"). Scan(&total).Error if err != nil { return 0, err @@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand } return total, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Table("project_flock_populations"). + Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Scan(&total).Error + if err != nil { + return 0, err + } + + if total < 0 { + total = 0 + } + + return int64(math.Round(total)), nil +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index e81d6cc5..dfc5e5d9 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse } func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { + // Tampilkan requested qty sebelum approve, consumed qty setelah approve + var displayQty float64 + if source.UsageQty > 0 { + // Sudah di-approve dan di-consume, tampilkan actual consumed quantity + displayQty = source.UsageQty + } else { + // Belum di-approve, tampilkan requested quantity + displayQty = source.RequestedQty + } + return LayingTransferSourceDTO{ SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), - Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity) + Qty: displayQty, ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), Note: source.Note, } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 9732ad75..3fe0b0b7 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -110,8 +110,32 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ offset := (params.Page - 1) * params.Limit transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) + // Apply search and filters + if params.Search != "" { + searchPattern := "%" + params.Search + "%" + db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). + Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). + Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) + } + + if params.TransferDate != "" { + db = db.Where("transfer_date::date = ?::date", params.TransferDate) + } + + if params.FlockSource > 0 { + db = db.Where("from_project_flock_id = ?", params.FlockSource) + } + + if params.FlockDestination > 0 { + db = db.Where("to_project_flock_id = ?", params.FlockDestination) + } + db = db.Order("created_at DESC") + + // Apply relations for eager loading + db = s.withRelations(db) + return db }) @@ -216,7 +240,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, sourceDetail := range req.SourceKandangs { if sourceDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0") + continue } totalSourceQty += sourceDetail.Quantity @@ -247,11 +271,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, targetDetail := range req.TargetKandangs { if targetDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0") + continue } totalTargetQty += targetDetail.Quantity } + if totalSourceQty == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0") + } + if totalTargetQty == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0") + } + if totalSourceQty != totalTargetQty { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) } @@ -279,11 +310,16 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity == 0 { + continue + } + productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] source := entity.LayingTransferSource{ LayingTransferId: createBody.Id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user UsageQty: 0, PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval ProductWarehouseId: &productWarehouseId, @@ -295,6 +331,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } for _, targetDetail := range req.TargetKandangs { + if targetDetail.Quantity == 0 { + continue + } targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) if err != nil { @@ -463,8 +502,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, source := entity.LayingTransferSource{ LayingTransferId: id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user UsageQty: 0, - PendingUsageQty: sourceDetail.Quantity, + PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval ProductWarehouseId: &productWarehouseId, } if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil { @@ -700,7 +740,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) } - note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber) + note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyTransferToLayingIn, StockableID: target.Id, @@ -814,15 +854,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project kandangAvailableQty := make(map[uint]float64) for _, kandang := range kandangs { - - totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) + // Gunakan fungsi repository yang sama dengan recording service + totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) if err != nil { - s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err) + s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err) kandangAvailableQty[kandang.Id] = 0 continue } - kandangAvailableQty[kandang.Id] = totalQty + kandangAvailableQty[kandang.Id] = totalAvailable } return pf, kandangAvailableQty, nil diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 45a73e48..06d52316 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -2,12 +2,12 @@ package validation type SourceKandangDetail struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + Quantity float64 `json:"quantity"` } type TargetKandangDetail struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + Quantity float64 `json:"quantity"` } type Create struct { @@ -29,8 +29,12 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty"` + TransferDate string `query:"transfer_date" validate:"omitempty"` + FlockSource uint `query:"flock_source" validate:"omitempty,number"` + FlockDestination uint `query:"flock_destination" validate:"omitempty,number"` } type Approve struct {