From ce108da847b163b7f6e6b2d29395c47146cc8914 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 15:36:06 +0700 Subject: [PATCH 1/5] FEAT[BE] :enhance production data calculations by adding TotalBirdSold and refining profit/loss metrics --- .../services/closingKeuangan.service.go | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index ca76c67e..804ca023 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -41,6 +41,7 @@ type ProductionData struct { TotalWeightProduced float64 TotalEggWeightKg float64 TotalWeightSold float64 + TotalBirdSold float64 TotalSalesAmount float64 } @@ -283,6 +284,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo continue } data.TotalWeightSold += delivery.TotalWeight + data.TotalBirdSold += delivery.UsageQty data.TotalSalesAmount += delivery.TotalPrice } @@ -383,46 +385,77 @@ func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *enti func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { - totalPopulationIn := production.TotalPopulationIn totalWeightProduced := production.TotalWeightProduced totalEggWeightKg := production.TotalEggWeightKg totalSalesAmount := production.TotalSalesAmount totalWeightSold := production.TotalWeightSold + totalBirdSold := production.TotalBirdSold + actualPopulation := production.TotalPopulationIn - production.TotalDepletion - weightForSales := totalWeightSold - weightForCalculation := totalWeightProduced - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - weightForSales = totalWeightSold - weightForCalculation = totalEggWeightKg - } + isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying) - calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { - if totalPopulationIn > 0 { - rpPerBird = amount / totalPopulationIn - } - if weightForSales > 0 { - rpPerKg = amount / weightForSales + // Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual + calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if isLaying { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + } else { + if totalBirdSold > 0 { + rpPerBird = amount / totalBirdSold + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } } return } - actualPopulation := production.TotalPopulationIn - production.TotalDepletion - - calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + // Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi + calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } - if weightForCalculation > 0 { - rpPerKg = amount / weightForCalculation + if isLaying { + if totalEggWeightKg > 0 { + rpPerKg = amount / totalEggWeightKg + } + } else { + if totalWeightProduced > 0 { + rpPerKg = amount / totalWeightProduced + } + } + return + } + + // Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual + calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if isLaying { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + } else { + if totalBirdSold > 0 { + rpPerBird = amount / totalBirdSold + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } } return } plItems := []dto.ProfitLossItem{} - salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) + salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + if isLaying { salesLabel = "Penjualan Telur" } plItems = append(plItems, dto.ToProfitLossItem( @@ -435,23 +468,23 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj )) totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost - _, sapronakRpPerKg := calculateMetrics(totalSapronakAmount) sapronakRpPerBird := 0.0 + sapronakRpPerKg := 0.0 for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { - rpPerBird, _ := calculateMetrics(amount) + rpPerBird, rpPerKg := calculateCostMetrics(amount) sapronakRpPerBird += rpPerBird + sapronakRpPerKg += rpPerKg } - sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), - sapronakLabel, + "Pengeluaran Sapronak", "purchase", sapronakRpPerBird, sapronakRpPerKg, totalSapronakAmount, )) - overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational) + overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", @@ -461,7 +494,7 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj costs.RealizationOperational, )) - ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", From 1c1f2f03aaf12b4b300f9bf6acafc8a7b3c9eaef Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 16:46:33 +0700 Subject: [PATCH 2/5] FIX[BE] :remove unused product warehouse repository import and streamline stock consumption logic in consumeDeliveryStock method --- .../services/deliveryorder.service.go | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 51e37465..a5eaf856 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -10,7 +10,6 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" - productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" @@ -502,69 +501,24 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor Tx: tx, }) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err)) + } + deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) - totalConsumed := 0.0 - var fifoConsumed float64 - var directConsumed float64 - - if result != nil && result.UsageQuantity > 0 { - fifoConsumed = result.UsageQuantity - totalConsumed = result.UsageQuantity - } - - if err != nil || (totalConsumed < requestedQty) { - remainder := requestedQty - totalConsumed - - pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) - pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) - if err2 != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock") - } - - if pw == nil || pw.Quantity < remainder { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. FIFO: %.2f, Direct Available: %.2f, Total Needed: %.2f", func() float64 { - if pw != nil { - return pw.Quantity - } else { - return 0 - } - }(), remainder, requestedQty)) - } - - if err := pwRepo.AdjustQuantities(ctx, map[uint]float64{ - marketingProduct.ProductWarehouseId: -remainder, - }, func(db *gorm.DB) *gorm.DB { - return tx - }); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to adjust product warehouse quantity") - } - - directConsumed = remainder - totalConsumed += remainder - } - - if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, totalConsumed, 0); err != nil { + if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil { 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) - } - + if actorID > 0 && result.UsageQuantity > 0 { decreaseLog := &entity.StockLog{ - Decrease: totalConsumed, + Decrease: result.UsageQuantity, LoggableType: string(utils.StockLogTypeMarketing), LoggableId: deliveryProduct.Id, ProductWarehouseId: marketingProduct.ProductWarehouseId, CreatedBy: actorID, - Notes: notes, + Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { @@ -572,8 +526,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] - decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Stock -= decreaseLog.Decrease + decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease } else { decreaseLog.Stock -= decreaseLog.Decrease } From e406b20ca7e41e70f1fceb8019dfcfeaa408441d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 21:11:27 +0700 Subject: [PATCH 3/5] FEAT[BE] :Fixing fifo stock when marketing deleted --- internal/modules/marketing/module.go | 2 +- .../services/deliveryorder.service.go | 13 +-- .../marketing/services/salesorder.service.go | 107 +++++++++++++----- 3 files changed, 83 insertions(+), 39 deletions(-) diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go index 2f8ea4fb..2dde163f 100644 --- a/internal/modules/marketing/module.go +++ b/internal/modules/marketing/module.go @@ -64,7 +64,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) - salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate) + salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index a5eaf856..1d0a9481 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -563,6 +563,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor return err } + if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { + return err + } + if actorID > 0 && currentUsage > 0 { increaseLog := &entity.StockLog{ Increase: currentUsage, @@ -570,7 +574,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor LoggableId: deliveryProduct.Id, ProductWarehouseId: marketingProduct.ProductWarehouseId, CreatedBy: actorID, - Notes: "", + Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { @@ -578,8 +582,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] - increaseLog.Stock = latestStockLog.Stock - increaseLog.Stock += increaseLog.Increase + increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase } else { increaseLog.Stock += increaseLog.Increase } @@ -587,9 +590,5 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil) } - if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { - return err - } - return nil } diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index 9d950307..df75fe82 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -19,6 +19,7 @@ import ( userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -41,11 +42,12 @@ type salesOrdersService struct { ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService + FifoSvc commonSvc.FifoService WarehouseRepo warehouseRepo.WarehouseRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } -func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository, +func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, @@ -55,6 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome ProductWarehouseRepo: productWarehouseRepo, UserRepo: userRepo, ApprovalSvc: approvalSvc, + FifoSvc: fifoSvc, WarehouseRepo: warehouseRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, } @@ -230,14 +233,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } if len(req.MarketingProducts) > 0 { - for _, item := range req.MarketingProducts { - if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { - return nil, err - } - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, - ); err != nil { - return nil, err + for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err } } } @@ -333,6 +336,32 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u totalPrice = totalWeight * rp.UnitPrice } + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + } + + if err == nil && deliveryProduct.Id != 0 { + oldQty := old.Qty + newQty := rp.Qty + qtyDiff := newQty - oldQty + + if qtyDiff < 0 { + return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.") + } else if qtyDiff > 0 { + _, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: deliveryProduct.Id, + ProductWarehouseID: rp.ProductWarehouseId, + Quantity: qtyDiff, + Tx: dbTransaction, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err)) + } + } + } + updateBody := map[string]any{ "product_warehouse_id": rp.ProductWarehouseId, "qty": rp.Qty, @@ -345,25 +374,20 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") } - if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - mdp := &entity.MarketingDeliveryProduct{ - MarketingProductId: old.Id, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: rp.VehicleNumber, - UsageQty: 0, - PendingQty: 0, - } - if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") - } - } else { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + if deliveryProduct.Id == 0 { + mdp := &entity.MarketingDeliveryProduct{ + MarketingProductId: old.Id, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + UsageQty: 0, + PendingQty: 0, + } + if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") } } } else { @@ -380,10 +404,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") } - if err == nil { + if err == nil && deliveryProduct.Id != 0 { - if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) + if deliveryProduct.DeliveryDate != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id)) + } + + if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: deliveryProduct.Id, + Tx: dbTransaction, + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err)) } if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { @@ -459,6 +491,19 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) if len(marketing.Products) > 0 { + deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id) + if err == nil && len(deliveryProducts) > 0 { + for _, dp := range deliveryProducts { + if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: dp.Id, + Tx: dbTransaction, + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err)) + } + } + } + for _, product := range marketing.Products { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("marketing_product_id = ?", product.Id).Unscoped() From 58ae03a090af42a3aa9b6013f4c8927d8fe94735 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 21:45:18 +0700 Subject: [PATCH 4/5] FIX[BE] :remove unnecessary quantity calculations in Adjustment method to streamline stock adjustment logic --- .../inventory/adjustments/services/adjustment.service.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index ceefcb1e..862d6991 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -160,7 +160,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } - afterQuantity := productWarehouse.Quantity newLog := &entity.StockLog{ LoggableType: string(utils.StockLogTypeAdjustment), LoggableId: 0, @@ -183,14 +182,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } if transactionType == string(utils.StockLogTransactionTypeIncrease) { - afterQuantity += req.Quantity newLog.Increase = req.Quantity newLog.Stock += newLog.Increase } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) } - afterQuantity -= req.Quantity newLog.Decrease = req.Quantity newLog.Stock -= newLog.Decrease } @@ -243,12 +240,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } } - productWarehouse.Quantity = afterQuantity - if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return err - } - createdAdjustmentStockId = adjustmentStock.Id return nil }) From 9a328ae1e44d914ad0774f8c91278f80fa39dded Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 08:06:52 +0700 Subject: [PATCH 5/5] FEAT[BE] :implement proportional distribution with rounding for stock allocation in transfer laying approval process --- .../services/deliveryorder.service.go | 1 + .../services/transfer_laying.service.go | 85 ++++++++++++------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 1d0a9481..6d9392a6 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -520,6 +520,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor CreatedBy: actorID, Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity), } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") 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 e6e9a862..15351e56 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "strings" "time" @@ -743,7 +744,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - + stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction) @@ -817,6 +818,27 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") } + targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare) + + for i, target := range targets { + roundedQty := math.Round(targetShares[i]) + if roundedQty <= 0 { + continue + } + mappingAllocation := &entity.StockAllocation{ + StockableType: fifo.UsableKeyTransferToLayingOut.String(), + StockableId: source.Id, + UsableType: fifo.StockableKeyTransferToLayingIn.String(), + UsableId: target.Id, + ProductWarehouseId: *source.ProductWarehouseId, + Qty: roundedQty, + Status: entity.StockAllocationStatusActive, + } + if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target") + } + } + stockLogDecrease := &entity.StockLog{ ProductWarehouseId: *source.ProductWarehouseId, CreatedBy: actorID, @@ -937,36 +959,6 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi return err } -func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { - - productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx) - - existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) - if err == nil && existing != nil { - - if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil { - return nil, err - } - return existing, nil - } - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - - newWarehouse := &entity.ProductWarehouse{ - ProductId: productID, - WarehouseId: warehouseID, - ProjectFlockKandangId: projectFlockKandangId, - Quantity: quantity, - } - - if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { - return nil, err - } - - return newWarehouse, nil -} - 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 { @@ -1060,3 +1052,34 @@ func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFl return kandangMaxTargetQty, nil } + +func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 { + if len(targets) == 0 { + return []float64{} + } + + targetShares := make([]float64, len(targets)) + totalRounded := 0.0 + + for i, target := range targets { + targetShares[i] = (target.TotalQty / totalTargetQty) * sourceShare + totalRounded += math.Round(targetShares[i]) + } + + diff := sourceShare - totalRounded + + if diff != 0 { + maxIdx := 0 + maxDecimal := 0.0 + for i, share := range targetShares { + decimal := share - math.Round(share) + if decimal > maxDecimal { + maxDecimal = decimal + maxIdx = i + } + } + targetShares[maxIdx] += diff + } + + return targetShares +}