feat[BE]: update HPP calculations to use totalWeightProduced and totalActualPopulation

This commit is contained in:
aguhh18
2025-12-18 10:45:04 +07:00
parent 1b23861656
commit 096a446450
4 changed files with 62 additions and 47 deletions
@@ -96,7 +96,7 @@ func getFlagLabel(flagType utils.FlagType) string {
return "Pembelian " + string(flagType)
}
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem {
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightProduced, totalPopulation float64) []HppItem {
flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
@@ -125,7 +125,7 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe
if isValid && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
@@ -144,14 +144,14 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup {
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppGroup {
items := []HppItem{}
// Overhead: all budgets vs (all expenses EXCEPT ekspedisi)
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightSold)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold)
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced)
if budgetAmount > 0 || realizationAmount > 0 {
items = append(items, HppItem{
@@ -165,7 +165,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti
// Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold)
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced)
if ekspedisiAmount > 0 {
items = append(items, HppItem{
@@ -185,7 +185,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti
// === HPP SUMMARY ===
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp {
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) SummaryHpp {
// Budget: purchases + budgets
purchaseTotal := sumPurchaseTotal(purchaseItems)
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
@@ -194,8 +194,8 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
// Realization: all expenses
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold)
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightProduced)
return SummaryHpp{
Label: label,
@@ -206,16 +206,16 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
}
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection {
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: "HPP dan Pengeluaran",
Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation),
Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation),
},
ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation),
ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation),
}
summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation)
summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation)
return HppPurchasesSection{
Hpp: hppGroups,
@@ -266,37 +266,33 @@ func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.M
return items
}
func ToPembelianItems(purchases []entities.PurchaseItem, totalPopulation, totalWeightSold float64) []PLItem {
amount := sumPurchasesByFilter(purchases, func(item *entities.PurchaseItem) bool {
if item.Product == nil || len(item.Product.Flags) == 0 {
return false
}
for _, flag := range item.Product.Flags {
flagType := strings.ToUpper(flag.Name)
if flagType == string(utils.FlagDOC) || flagType == string(utils.FlagOVK) || flagType == string(utils.FlagPakan) {
return true
}
}
return false
})
func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem {
// Calculate total cost using same logic as report penjualan:
// Total Cost = All Purchase Items + All BOP Expenses
purchaseAmount := sumPurchaseTotal(purchases)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold)
// Get BOP expenses (all expenses except ekspedisi)
bopAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
totalCost := purchaseAmount + bopAmount
rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced)
return []PLItem{
ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)),
ToPLItem("Harga Pokok Penjualan (HPP)", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)),
}
}
func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem {
func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem {
realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold)
rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced)
return []PLItem{
ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)),
}
}
func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem {
func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem {
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced)
return []PLItem{
ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)),
}
@@ -348,7 +344,7 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec
// === MAIN BUILDER ===
func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse {
func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced float64) ReportResponse {
var totalPopulation float64
var totalWeightSold float64
@@ -360,12 +356,13 @@ func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entiti
totalWeightSold += delivery.TotalWeight
}
hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation)
// Use totalWeightProduced for HPP calculation (not totalWeightSold)
hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation)
penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold)
pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold)
overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold)
ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold)
pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, totalPopulation, totalWeightProduced)
overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightProduced)
ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightProduced)
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
return ToReportResponse(hppSection, plSection)
@@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto
}
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string)
@@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
for nonstockID, overhead := range overheadsByNonstockID {
overhead.ActualDate = latestDateByNonstockID[nonstockID]
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation)
if overhead.ActualQuantity > 0 {
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
@@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
BudgetTotalAmount: totalBudgetAmount,
ActualQuantity: totalActualQuantity,
ActualTotalAmount: totalActualAmount,
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation),
},
Overheads: overheadItems,
}
@@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 {
return qty * price
}
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
if totalChickinQty > 0 {
return totalPrice / totalChickinQty
func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 {
if totalActualPopulation > 0 {
return totalPrice / totalActualPopulation
}
return 0
}
+3 -1
View File
@@ -13,6 +13,7 @@ import (
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
@@ -31,11 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -15,6 +15,7 @@ import (
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -47,9 +48,10 @@ type closingService struct {
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
@@ -62,6 +64,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo,
}
}
@@ -379,7 +382,14 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
totalChickinQty += chickin.UsageQty
}
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
return &result, nil
}
@@ -435,7 +445,12 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins)
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced)
return &report, nil
}