Merge branch 'fix/hpp-calculate' into 'development'

[FIX][BE]: create common service calculate hpp

See merge request mbugroup/lti-api!241
This commit is contained in:
Adnan Zahir
2026-01-23 16:16:52 +07:00
5 changed files with 594 additions and 211 deletions
+3 -1
View File
@@ -32,6 +32,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
chickinRepository := chickinRepo.NewChickinRepository(db)
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db)
hppCostRepository := commonRepo.NewHppCostRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
@@ -43,7 +44,8 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
hppSvc := approvalService.NewHppService(hppCostRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, hppSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -6,7 +6,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
@@ -24,9 +23,9 @@ type HppPerKandangRow struct {
// RemainingChickenBirds float64
// RemainingChickenWeight float64
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
// EggProductionPiecesRemaining float64
// EggProductionTotalWeightKg float64
// EggProductionTotalPieces float64
}
type HppPerKandangCostRow struct {
@@ -50,7 +49,7 @@ type HppPerKandangSupplierRow struct {
type HppPerKandangRepository interface {
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
GetWeightRemainingByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
}
type hppPerKandangRepository struct {
@@ -133,60 +132,6 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
var rows []HppPerKandangCostRow
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
pfk.id AS project_flock_kandang_id,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS feed_cost,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS ovk_cost`,
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
query = query.Group("pfk.id").Order("pfk.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, nil, err
}
docRows := make([]struct {
ProjectFlockKandangID uint
DocCost float64
@@ -262,130 +207,10 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
}
}
budgetRows := make([]struct {
ProjectFlockKandangID uint
BudgetCost float64
}, 0)
pfkUsageSub := r.db.
Table("project_chickins AS pc").
Select(`
pc.project_flock_kandang_id,
SUM(pc.usage_qty) AS kandang_usage_qty`).
Group("pc.project_flock_kandang_id")
projectUsageSub := r.db.
Table("project_chickins AS pc").
Select(`
pfk.project_flock_id,
SUM(pc.usage_qty) AS project_usage_qty`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Group("pfk.project_flock_id")
budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
pfk.id AS project_flock_kandang_id,
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("pfk.id")
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
return nil, nil, err
}
for _, budget := range budgetRows {
entry, ok := costMap[budget.ProjectFlockKandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: budget.ProjectFlockKandangID,
})
entry = &rows[len(rows)-1]
costMap[budget.ProjectFlockKandangID] = entry
}
entry.BudgetCost += budget.BudgetCost
}
expenseRows := make([]struct {
ProjectFlockKandangID uint
ExpenseCost float64
}, 0)
expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
pfk.id AS project_flock_kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("pfk.id")
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
return nil, nil, err
}
for _, exp := range expenseRows {
entry, ok := costMap[exp.ProjectFlockKandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: exp.ProjectFlockKandangID,
})
entry = &rows[len(rows)-1]
costMap[exp.ProjectFlockKandangID] = entry
}
entry.ExpenseCost += exp.ExpenseCost
}
feedSuppliers := make([]HppPerKandangSupplierRow, 0)
feedQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL")
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
return nil, nil, err
}
for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID,
})
costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1]
}
feedSuppliers[i].Category = "FEED"
}
supplierRows := append(docSuppliers, feedSuppliers...)
return rows, supplierRows, nil
return rows, docSuppliers, nil
}
func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
func (r *hppPerKandangRepository) GetWeightRemainingByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
if len(projectFlockKandangIDs) == 0 {
return map[uint]HppPerKandangRow{}, nil
}
@@ -406,9 +231,9 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
type eggRow struct {
ProjectFlockKandangID uint
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
// EggProductionPiecesRemaining float64
// EggProductionTotalWeightKg float64
// EggProductionTotalPieces float64
}
eggRows := make([]eggRow, 0)
@@ -416,10 +241,7 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
Table("recordings AS r").
Select(`
r.project_flock_kandangs_id AS project_flock_kandang_id,
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
COALESCE((SUM(re.weight) / NULLIF(SUM(re.total_qty), 0)) * SUM(re.total_qty - re.total_used), 0) AS egg_production_weight_kg_remaining`).
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
@@ -437,9 +259,9 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
result[row.ProjectFlockKandangID] = HppPerKandangRow{
ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
EggProductionTotalPieces: row.EggProductionTotalPieces,
// EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
// EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
// EggProductionTotalPieces: row.EggProductionTotalPieces,
}
}
@@ -57,6 +57,7 @@ type repportService struct {
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
HppSvc approvalService.HppService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
@@ -85,6 +86,7 @@ func NewRepportService(
chickinRepo chickinRepo.ProjectChickinRepository,
recordingRepo recordingRepo.RecordingRepository,
approvalSvc approvalService.ApprovalService,
hppSvc approvalService.HppService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
debtSupplierRepo repportRepo.DebtSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
@@ -104,6 +106,7 @@ func NewRepportService(
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
HppSvc: hppSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
@@ -1573,16 +1576,16 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
return nil, nil, err
}
eggMap, err := s.HppPerKandangRepo.GetEggProductionByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
eggMap, err := s.HppPerKandangRepo.GetWeightRemainingByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
if err != nil {
return nil, nil, err
}
for pfkID, egg := range eggMap {
if rowIdx, ok := pfkIndex[pfkID]; ok {
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
// repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
// repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
// repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
}
}
}
@@ -1590,12 +1593,12 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows {
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
FeedCost: row.FeedCost,
OvkCost: row.OvkCost,
DocCost: row.DocCost,
DocQty: row.DocQty,
BudgetCost: row.BudgetCost,
ExpenseCost: row.ExpenseCost,
// FeedCost: row.FeedCost,
// OvkCost: row.OvkCost,
DocCost: row.DocCost,
DocQty: row.DocQty,
// BudgetCost: row.BudgetCost,
// ExpenseCost: row.ExpenseCost,
}
}
@@ -1669,19 +1672,33 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
continue
}
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
var eggPiecesFloatRemaining float64
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
var eggTotalPiecesFloat float64
var eggWeightFloat float64
eggHpp := 0.0
if s.HppSvc != nil {
hppCost, err := s.HppSvc.CalculateHppCost(row.ProjectFlockKandangID, &endOfDay)
if err != nil {
return nil, nil, err
}
if hppCost != nil {
// eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg
eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir
eggHpp = hppCost.Estimation.HargaKg
eggTotalPiecesFloat = hppCost.Estimation.Butir
eggWeightFloat = hppCost.Estimation.Kg
}
}
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
eggPiecesFloatRemaining = 0
}
eggTotalPiecesFloat := row.EggProductionTotalPieces
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
eggTotalPiecesFloat = 0
}
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
eggRemainingWeightFloatRemaining = 0
}
eggWeightFloat := row.EggProductionTotalWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0
}
@@ -1705,11 +1722,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
costEntry := costMap[row.ProjectFlockKandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
eggHpp := 0.0
if eggWeightFloat > 0 {
eggHpp = (totalCost / eggWeightFloat) / 1000
}
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)