diff --git a/db_lti_erp-202601271102-stg.sql b/db_lti_erp-202601271102-stg.sql new file mode 100644 index 00000000..2a8495d8 Binary files /dev/null and b/db_lti_erp-202601271102-stg.sql differ diff --git a/internal/common/repository/common.hpp.repository.go b/internal/common/repository/common.hpp.repository.go index 97ad3800..e0f2bcc5 100644 --- a/internal/common/repository/common.hpp.repository.go +++ b/internal/common/repository/common.hpp.repository.go @@ -20,6 +20,7 @@ type HppCostRepository interface { GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) + GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) @@ -219,6 +220,24 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang return totals.TotalPieces, totals.TotalWeightKg, nil } +func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { + var totals struct { + TotalPieces float64 + TotalWeightKg float64 + } + err := r.db.WithContext(ctx). + Table("recordings AS r"). + Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg"). + Joins("JOIN recording_eggs AS re ON re.recording_id = r.id"). + Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). + Scan(&totals).Error + if err != nil { + return 0, 0, err + } + + return totals.TotalPieces, totals.TotalWeightKg, nil +} + func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds( ctx context.Context, projectFlockKandangIDs []uint, diff --git a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.down.sql b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.down.sql new file mode 100644 index 00000000..5c44b309 --- /dev/null +++ b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.down.sql @@ -0,0 +1,2 @@ +-- Drop transfer laying sequence +DROP SEQUENCE IF EXISTS transfer_laying_seq; \ No newline at end of file diff --git a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql new file mode 100644 index 00000000..f5f5bdf7 --- /dev/null +++ b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql @@ -0,0 +1,33 @@ +-- Create sequence for transfer laying movement number +CREATE SEQUENCE transfer_laying_seq START +WITH + 1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE; + +-- Set sequence starting value based on existing data (if any) +-- This prevents duplicate movement numbers if there's already data +DO $$ DECLARE max_existing INTEGER; + +BEGIN +-- Check if table exists and has data +IF EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE + table_schema = 'public' + AND table_name = 'transfer_to_layings' +) THEN +-- Get max ID from existing records +SELECT COALESCE(MAX(id), 0) INTO max_existing +FROM transfer_to_layings; + +-- Set sequence to start after the highest existing ID +IF max_existing > 0 THEN PERFORM setval ( + 'transfer_laying_seq', + max_existing +); + +END IF; + +END IF; + +END $$; \ No newline at end of file diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index ca76c67e..44137fad 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -262,7 +262,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo } if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil) + _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(c.Context(), projectFlockKandangIDs) if err != nil { data.TotalEggWeightKg = 0 } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index e74332bc..1d69de6f 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -232,11 +232,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } for _, delivery := range req.Deliveries { - // Skip supplier validation if SupplierID is 0 (optional) + if delivery.SupplierID == 0 { continue } + if delivery.VehiclePlate == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Vehicle plate wajib diisi ketika supplier dipilih") + } + if delivery.DriverName == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Driver name wajib diisi ketika supplier dipilih") + } + if delivery.DeliveryCost <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost harus lebih dari 0 ketika supplier dipilih") + } + if delivery.DeliveryCostPerItem <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery cost per item harus lebih dari 0 ketika supplier dipilih") + } + supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/transfers/validations/transfer.validation.go b/internal/modules/inventory/transfers/validations/transfer.validation.go index e2f357f3..89676a41 100644 --- a/internal/modules/inventory/transfers/validations/transfer.validation.go +++ b/internal/modules/inventory/transfers/validations/transfer.validation.go @@ -21,11 +21,11 @@ type TransferDeliveryProduct struct { } type TransferDelivery struct { - DeliveryCost float64 `json:"delivery_cost" validate:"required"` - DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"` + DeliveryCost float64 `json:"delivery_cost"` + DeliveryCostPerItem float64 `json:"delivery_cost_per_item"` DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"` - DriverName string `json:"driver_name" validate:"required"` - VehiclePlate string `json:"vehicle_plate" validate:"required"` + DriverName string `json:"driver_name"` + VehiclePlate string `json:"vehicle_plate"` SupplierID uint `json:"supplier_id" ` Products []TransferDeliveryProduct `json:"products" validate:"required,dive"` } diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index ebf63252..b3d7e7bc 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -16,6 +17,10 @@ type TransferLayingRepository interface { // Tambah method baru untuk query dengan filter lengkap GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) + + // Get sequence for movement number + GetNextMovementNumber(ctx context.Context) (int64, error) + GenerateMovementNumber(ctx context.Context) (string, error) } type TransferLayingRepositoryImpl struct { @@ -29,6 +34,26 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { db: db, } } + +func (r *TransferLayingRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) { + var seq int64 + err := r.db.WithContext(ctx).Raw("SELECT nextval('transfer_laying_seq')").Scan(&seq).Error + if err != nil { + return 0, err + } + return seq, nil +} + +func (r *TransferLayingRepositoryImpl) GenerateMovementNumber(ctx context.Context) (string, error) { + seq, err := r.GetNextMovementNumber(ctx) + if err != nil { + return "", err + } + // Format: TL00001, TL00002, dst + movementNumber := fmt.Sprintf("TL%05d", seq) + return movementNumber, nil +} + func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.LayingTransfer](ctx, r.db, id) } 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 310391c6..396944e1 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -271,7 +271,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) 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, err := s.Repository.GenerateMovementNumber(c.Context()) + if err != nil { + s.Log.Errorf("Failed to generate movement number: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer") + } createBody := &entity.LayingTransfer{ TransferNumber: transferNumber, @@ -440,15 +444,105 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found") } + sourceKandangIDs := make([]uint, len(req.SourceKandangs)) + for i, detail := range req.SourceKandangs { + sourceKandangIDs[i] = detail.ProjectFlockKandangId + } + + if err := s.validateKandangOwnership( + c.Context(), + req.SourceProjectFlockId, + sourceKandangIDs, + ); err != nil { + return nil, err + } + + targetKandangIDs := make([]uint, len(req.TargetKandangs)) + for i, detail := range req.TargetKandangs { + targetKandangIDs[i] = detail.ProjectFlockKandangId + } + + if err := s.validateKandangOwnership( + c.Context(), + req.TargetProjectFlockId, + targetKandangIDs, + ); err != nil { + return nil, err + } + transferDate, err := time.Parse("2006-01-02", req.TransferDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") } + var totalSourceQty, totalTargetQty float64 + sourceWarehouseMap := make(map[uint]uint) + + for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity <= 0 { + continue + } + totalSourceQty += sourceDetail.Quantity + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + if err != nil { + return nil, err + } + + var totalPopulation float64 + var productWarehouseId uint + for _, pop := range populations { + totalPopulation += pop.TotalQty + if productWarehouseId == 0 { + productWarehouseId = pop.ProductWarehouseId + } + } + + if totalPopulation == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId)) + } + + if 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 + } + + for _, targetDetail := range req.TargetKandangs { + if targetDetail.Quantity <= 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)) + } + + // Ambil productWarehouseId pertama dari source yang valid (quantity > 0) + var firstProductWarehouseId uint + for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity > 0 { + if pwId, ok := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]; ok { + firstProductWarehouseId = pwId + break + } + } + } + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction) targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction) + pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction) // Hapus old sources dan targets for _, oldSource := range existingTransfer.Sources { @@ -472,26 +566,11 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, // Create new sources dengan pending quantity for _, sourceDetail := range req.SourceKandangs { - populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations") + if sourceDetail.Quantity == 0 { + continue } - if len(populations) == 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId)) - } - - var productWarehouseId uint - for _, pop := range populations { - if pop.ProductWarehouseId > 0 { - productWarehouseId = pop.ProductWarehouseId - break - } - } - - if productWarehouseId == 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId)) - } + productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] source := entity.LayingTransferSource{ LayingTransferId: id, @@ -506,7 +585,18 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, } } - pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction) + // Ambil product ID dari source warehouse pertama yang valid + var sourceProductID uint + if firstProductWarehouseId > 0 { + sourcePW, err := pwRepo.GetByID(c.Context(), firstProductWarehouseId, nil) + if err == nil { + sourceProductID = sourcePW.ProductId + } + } + + if sourceProductID == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse") + } for _, targetDetail := range req.TargetKandangs { targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) @@ -522,23 +612,6 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") } - // Ambil product ID dari source yang pertama (semua sources seharusnya product-nya sama) - var sourceProductID uint - 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 errors.Is(err, gorm.ErrRecordNotFound) {