Merge branch 'development' into 'staging'

Development

See merge request mbugroup/lti-api!285
This commit is contained in:
Adnan Zahir
2026-01-30 11:42:14 +07:00
13 changed files with 328 additions and 107 deletions
Binary file not shown.
@@ -20,6 +20,7 @@ type HppCostRepository interface {
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error)
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, 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) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error)
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
@@ -219,6 +220,24 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang
return totals.TotalPieces, totals.TotalWeightKg, nil 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( func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(
ctx context.Context, ctx context.Context,
projectFlockKandangIDs []uint, projectFlockKandangIDs []uint,
@@ -0,0 +1,2 @@
-- Drop transfer laying sequence
DROP SEQUENCE IF EXISTS transfer_laying_seq;
@@ -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 $$;
@@ -101,6 +101,26 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
} }
} }
func ToSalesAgeDTO(e entity.MarketingDeliveryProduct) SalesDTO {
productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags))
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
productFlags[i] = f.Name
}
var category string
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
}
ageInDay, _ := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
return SalesDTO{
Age: ageInDay,
Qty: e.UsageQty,
}
}
func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO { func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO {
var totalSalesPrice, totalActualPrice, sumSales, sumActual float64 var totalSalesPrice, totalActualPrice, sumSales, sumActual float64
@@ -841,7 +841,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
} }
} }
age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID) age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
@@ -1028,38 +1028,24 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return &result, nil return &result, nil
} }
func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) { func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) (float64, error) {
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB { penjualan, err := s.MarketingDeliveryProductRepo.GetClosingPenjualanForAgeChickDataProduction(ctx, projectFlockID, projectFlockKandangID)
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins")
})
if err != nil { if err != nil {
return 0, err return 0, err
} }
acumulateAgeQty := 0.0
var ( totalQty := 0.0
totalQty float64 for _, v := range penjualan {
totalAgeWeeks float64 sale := dto.ToSalesAgeDTO(v)
) acumulateAgeQty += float64(sale.Age) * sale.Qty
totalQty += sale.Qty
for _, product := range deliveryProducts { }
if product.UsageQty == 0 { if totalQty > 0 {
continue averageAge := acumulateAgeQty / totalQty
} return averageAge, nil
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
totalQty += product.UsageQty
} }
if totalQty == 0 { return 0, err
return 0, nil
}
return totalAgeWeeks / totalQty, nil
} }
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) { func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
@@ -262,7 +262,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo
} }
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { 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 { if err != nil {
data.TotalEggWeightKg = 0 data.TotalEggWeightKg = 0
} }
@@ -232,11 +232,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
for _, delivery := range req.Deliveries { for _, delivery := range req.Deliveries {
// Skip supplier validation if SupplierID is 0 (optional)
if delivery.SupplierID == 0 { if delivery.SupplierID == 0 {
continue 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) supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -21,11 +21,11 @@ type TransferDeliveryProduct struct {
} }
type TransferDelivery struct { type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost" validate:"required"` DeliveryCost float64 `json:"delivery_cost"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"` DeliveryCostPerItem float64 `json:"delivery_cost_per_item"`
DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"` DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"`
DriverName string `json:"driver_name" validate:"required"` DriverName string `json:"driver_name"`
VehiclePlate string `json:"vehicle_plate" validate:"required"` VehiclePlate string `json:"vehicle_plate"`
SupplierID uint `json:"supplier_id" ` SupplierID uint `json:"supplier_id" `
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"` Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
} }
@@ -22,6 +22,7 @@ type MarketingDeliveryProductRepository interface {
UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error
GetUsageQty(ctx context.Context, id uint) (float64, error) GetUsageQty(ctx context.Context, id uint) (float64, error)
ResetFifoFields(ctx context.Context, id uint) error ResetFifoFields(ctx context.Context, id uint) error
GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
} }
type MarketingDeliveryProductRepositoryImpl struct { type MarketingDeliveryProductRepositoryImpl struct {
@@ -93,6 +94,46 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context
return deliveryProducts, nil return deliveryProducts, nil
} }
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("flags.name IN (?)", []string{
string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling),
string(utils.FlagPullet),
string(utils.FlagLayer),
}).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Distinct("marketing_delivery_products.*")
if projectFlockKandangID != nil {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
}
db = db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Order("marketing_delivery_products.delivery_date DESC")
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) { func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct var deliveryProducts []entity.MarketingDeliveryProduct
@@ -410,6 +410,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
} }
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
var itemDeliveryDate *time.Time var itemDeliveryDate *time.Time
if requestedProduct.DeliveryDate != "" { if requestedProduct.DeliveryDate != "" {
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
@@ -421,11 +422,8 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
itemDeliveryDate = deliveryProduct.DeliveryDate itemDeliveryDate = deliveryProduct.DeliveryDate
} }
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false isPakanOrOVK := false
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { if foundMarketingProduct.ProductWarehouse.Id != 0 && foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true isPakanOrOVK = true
@@ -506,60 +504,71 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
if err == nil && result.UsageQuantity > 0 { totalConsumed := 0.0
if actorID > 0 { var fifoConsumed float64
decreaseLog := &entity.StockLog{ var directConsumed float64
Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeMarketing), if result != nil && result.UsageQuantity > 0 {
LoggableId: deliveryProduct.Id, fifoConsumed = result.UsageQuantity
ProductWarehouseId: marketingProduct.ProductWarehouseId, totalConsumed = result.UsageQuantity
CreatedBy: actorID,
Notes: "",
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
}
} }
if err != nil { if err != nil || (totalConsumed < requestedQty) {
remainder := requestedQty - totalConsumed
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err2 != nil { if err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
} }
if pw == nil || pw.Quantity < requestedQty { if pw == nil || pw.Quantity < remainder {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. FIFO: %.2f, Direct Available: %.2f, Total Needed: %.2f", func() float64 {
if pw != nil { if pw != nil {
return pw.Quantity return pw.Quantity
} else { } else {
return 0 return 0
} }
}(), requestedQty)) }(), remainder, requestedQty))
} }
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil { if err := pwRepo.AdjustQuantities(ctx, map[uint]float64{
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") marketingProduct.ProductWarehouseId: -remainder,
}, func(db *gorm.DB) *gorm.DB {
return tx
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to adjust product warehouse quantity")
} }
if actorID > 0 { directConsumed = remainder
decreaseLog := &entity.StockLog{ totalConsumed += remainder
Decrease: requestedQty,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: "",
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
}
return nil
} }
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil { if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, totalConsumed, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
} }
if actorID > 0 && totalConsumed > 0 {
notes := ""
if fifoConsumed > 0 && directConsumed > 0 {
notes = fmt.Sprintf("Partial FIFO (%.2f) + Direct (%.2f)", fifoConsumed, directConsumed)
} else if fifoConsumed > 0 {
notes = fmt.Sprintf("FIFO stock only (%.2f)", fifoConsumed)
} else if directConsumed > 0 {
notes = fmt.Sprintf("Direct stock only (%.2f)", directConsumed)
}
decreaseLog := &entity.StockLog{
Decrease: totalConsumed,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: notes,
}
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
}
return nil return nil
} }
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -16,6 +17,10 @@ type TransferLayingRepository interface {
// Tambah method baru untuk query dengan filter lengkap // Tambah method baru untuk query dengan filter lengkap
GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) 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 { type TransferLayingRepositoryImpl struct {
@@ -29,6 +34,26 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
db: db, 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) { func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.LayingTransfer](ctx, r.db, id) return repository.Exists[entity.LayingTransfer](ctx, r.db, id)
} }
@@ -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)) 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{ createBody := &entity.LayingTransfer{
TransferNumber: transferNumber, 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") 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) transferDate, err := time.Parse("2006-01-02", req.TransferDate)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") 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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction) repoTx := s.Repository.WithTx(dbTransaction)
sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction) sourceRepo := s.LayingTransferSourceRepo.WithTx(dbTransaction)
targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction) targetRepo := s.LayingTransferTargetRepo.WithTx(dbTransaction)
pwRepo := rInventory.NewProductWarehouseRepository(dbTransaction)
// Hapus old sources dan targets // Hapus old sources dan targets
for _, oldSource := range existingTransfer.Sources { 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 // 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) if sourceDetail.Quantity == 0 {
if err != nil { continue
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations")
} }
if len(populations) == 0 { productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
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))
}
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: id, 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 { for _, targetDetail := range req.TargetKandangs {
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) 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") 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) 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) {