adjust common hpp v2

This commit is contained in:
giovanni
2026-04-19 17:27:42 +07:00
parent 69d6fc165a
commit 04aad18a4c
7 changed files with 1020 additions and 259 deletions
@@ -89,11 +89,21 @@ type HppV2ManualDepreciationInputRow struct {
Note *string
}
type HppV2FarmDepreciationSnapshotRow struct {
ID uint
ProjectFlockID uint
PeriodDate time.Time
DepreciationPercentEffective float64
DepreciationValue float64
PulletCostDayNTotal float64
}
type HppV2CostRepository interface {
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error)
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error)
GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error)
GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
@@ -239,6 +249,29 @@ func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
return &row, nil
}
func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(
ctx context.Context,
projectFlockID uint,
periodDate time.Time,
) (*HppV2FarmDepreciationSnapshotRow, error) {
var row HppV2FarmDepreciationSnapshotRow
err := r.db.WithContext(ctx).
Table("farm_depreciation_snapshots").
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total").
Where("project_flock_id = ?", projectFlockID).
Where("period_date = DATE(?)", periodDate).
Limit(1).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) {
type row struct {
ChickInDate *time.Time
@@ -327,11 +360,11 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
COALESCE(pi.product_id, ast_pw.product_id, 0) AS source_product_id,
COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name,
COALESCE(SUM(sa.qty), 0) AS qty,
CASE
COALESCE(MAX(CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0
END AS unit_price,
END), 0) AS unit_price,
COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
@@ -367,12 +400,7 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
sa.stockable_type,
sa.stockable_id,
COALESCE(pi.product_id, ast_pw.product_id, 0),
COALESCE(pi_prod.name, ast_prod.name, ''),
CASE
WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0)
ELSE 0
END
COALESCE(pi_prod.name, ast_prod.name, '')
`).
Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC").
Scan(&rows).Error
@@ -417,7 +445,7 @@ func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("ast.created_at <= ?", *date).
// Where("ast.created_at <= ?", *date).
Where("COALESCE(ast.total_qty, 0) > 0").
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames).
Order("ast.created_at ASC, ast.id ASC").
@@ -450,6 +478,15 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
usableStockTransferOut := fifo.UsableKeyStockTransferOut.String()
unitPriceExpr := fmt.Sprintf(`
CASE
WHEN sa.stockable_type = '%s' THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(ast.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(spi.price, sast.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(tpi.price, tast.price, 0)
ELSE 0
END
`, stockablePurchase, stockableAdjustment, stockableTransferIn, stockableTransferToLaying)
rows := make([]HppV2ChickinCostRow, 0)
query := r.db.WithContext(ctx).
@@ -479,30 +516,9 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
''
) AS source_product_name,
COALESCE(SUM(sa.qty), 0) AS qty,
CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0)
ELSE 0
END AS unit_price,
COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0)
ELSE 0
END), 0) AS total_cost
`,
stockablePurchase,
stockableAdjustment,
stockableTransferIn,
stockableTransferToLaying,
stockablePurchase,
stockableAdjustment,
stockableTransferIn,
stockableTransferToLaying,
).
`+unitPriceExpr+` AS unit_price,
COALESCE(SUM(sa.qty * (`+unitPriceExpr+`)), 0) AS total_cost
`).
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?",
usableProjectChickin,
@@ -563,7 +579,7 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
}
err := query.
Group(`
Group(fmt.Sprintf(`
pc.id,
pc.project_flock_kandang_id,
pc.chick_in_date,
@@ -587,14 +603,8 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
sast_prod.name,
''
),
CASE
WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0)
WHEN sa.stockable_type = '` + stockableTransferIn + `' THEN COALESCE(spi.price, sast.price, 0)
WHEN sa.stockable_type = '` + stockableTransferToLaying + `' THEN COALESCE(tpi.price, tast.price, 0)
ELSE 0
END
`).
%s
`, unitPriceExpr)).
Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC").
Scan(&rows).Error
if err != nil {
+37 -101
View File
@@ -2,7 +2,6 @@ package service
import (
"context"
"log"
"math"
"time"
@@ -40,108 +39,91 @@ func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
}
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
logHpp("CalculateHppCost", "start project_flock_kandang_id=%d input_date=%s", projectFlockKandangId, formatTimePtr(date))
if date == nil {
now := time.Now()
date = &now
}
logHpp("CalculateHppCost", "normalized_date=%s", formatTimePtr(date))
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
logHpp("CalculateHppCost", "load_location_error=%v", err)
return nil, err
}
logHpp("CalculateHppCost", "location=%s", location.String())
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour)
logHpp("CalculateHppCost", "start_of_day=%s end_of_day=%s", startOfDay.Format(time.RFC3339), endOfDay.Format(time.RFC3339))
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil {
logHpp("CalculateHppCost", "get_depresiasi_transfer_error=%v", err)
return nil, err
}
logHpp("CalculateHppCost", "depresiasi_transfer=%f", depresiasiTransfer)
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer)
if err != nil {
logHpp("CalculateHppCost", "get_total_production_cost_error=%v", err)
return nil, err
}
logHpp("CalculateHppCost", "total_production_cost=%f", totalProductionCost)
result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil {
logHpp("CalculateHppCost", "get_hpp_estimation_dan_realisasi_error=%v", err)
return nil, err
}
logHpp("CalculateHppCost", "done estimation=%+v real=%+v", result.Estimation, result.Real)
return result, nil
}
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) {
logHpp("GetTotalDepresiasiFlockGrowing", "start source_project_flock_id=%d input_date=%s", sourceProjectFlockID, formatTimePtr(date))
if date == nil {
now := time.Now()
date = &now
}
logHpp("GetTotalDepresiasiFlockGrowing", "normalized_date=%s", formatTimePtr(date))
if s.hppRepo == nil {
logHpp("GetTotalDepresiasiFlockGrowing", "repo_nil return=0")
return 0, nil
}
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_project_flock_kandang_ids_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "kandang_ids=%v", kandangIDs)
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_doc_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "doc_cost=%f", docCost)
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_budget_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "budget_cost=%f", budgetCost)
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_expedision_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "expedision_cost=%f", expedisionCost)
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_feed_usage_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "feed_cost=%f", feedCost)
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_ovk_usage_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalDepresiasiFlockGrowing", "ovk_cost=%f", ovkCost)
total := docCost + budgetCost + expedisionCost + feedCost + ovkCost
logHpp("GetTotalDepresiasiFlockGrowing", "done total=%f", total)
return total, nil
}
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) {
logHpp("GetTotalProductionCost", "start project_flock_kandang_id=%d end_date=%s depresiasi_transfer=%f", projectFlockKandangId, formatTimePtr(endDate), depresiasiTransfer)
// if date == nil {
// now := time.Now()
// date = &now
@@ -149,248 +131,210 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil {
logHpp("GetTotalProductionCost", "get_pullet_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalProductionCost", "cost_pullet=%f", costPullet)
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
logHpp("GetTotalProductionCost", "get_feed_usage_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalProductionCost", "cost_feed=%f", costFeed)
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
logHpp("GetTotalProductionCost", "get_ovk_usage_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalProductionCost", "cost_ovk=%f", costOvk)
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
if err != nil {
logHpp("GetTotalProductionCost", "get_expedision_cost_error=%v", err)
return 0, err
}
logHpp("GetTotalProductionCost", "cost_expedision=%f", costExpedision)
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate)
if err != nil {
logHpp("GetTotalProductionCost", "get_budget_kandang_laying_error=%v", err)
return 0, err
}
logHpp("GetTotalProductionCost", "cost_budget=%f", costBudget)
// fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer)
// depresiasiTransfer = 0
total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget
logHpp("GetTotalProductionCost", "done total=%f", total)
return total, nil
}
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
logHpp("GetBudgetKandangLaying", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate))
// if date == nil {
// now := time.Now()
// date = &now
// }
if s.hppRepo == nil {
logHpp("GetBudgetKandangLaying", "repo_nil return=0")
return 0, nil
}
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_id_error=%v", err)
return 0, err
}
logHpp("GetBudgetKandangLaying", "project_flock_id=%d", projectFlockId)
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_kandang_ids_error=%v", err)
return 0, err
}
logHpp("GetBudgetKandangLaying", "project_flock_kandang_ids=%v", projectFlockKandangIds)
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate)
if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_flock_error=%v", err)
return 0, err
}
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock=%f", eggProduksiPiecesFlock)
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_kandang_error=%v", err)
return 0, err
}
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_kandang=%f", eggProduksiPiecesKandang)
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
if err != nil {
logHpp("GetBudgetKandangLaying", "get_budget_cost_error=%v", err)
return 0, err
}
logHpp("GetBudgetKandangLaying", "total_budget_cost=%f", totalBudgetCost)
if eggProduksiPiecesFlock == 0 {
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock_zero return=0")
return 0, nil
}
result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock
logHpp("GetBudgetKandangLaying", "done result=%f", result)
return result, nil
}
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
logHpp("GetDepresiasiTransfer", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate))
if endDate == nil {
now := time.Now()
endDate = &now
}
logHpp("GetDepresiasiTransfer", "normalized_end_date=%s", formatTimePtr(endDate))
if s.hppRepo == nil {
logHpp("GetDepresiasiTransfer", "repo_nil return=0")
return 0, nil
}
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil {
logHpp("GetDepresiasiTransfer", "get_transfer_source_summary_error=%v", err)
return 0, err
}
logHpp("GetDepresiasiTransfer", "source_project_flock_id=%d transfer_total_qty=%f", sourceProjectFlockID, transferTotalQty)
if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
logHpp("GetDepresiasiTransfer", "use_manual_fallback=true")
result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId)
if fallbackErr != nil {
logHpp("GetDepresiasiTransfer", "manual_fallback_error=%v", fallbackErr)
return 0, fallbackErr
}
logHpp("GetDepresiasiTransfer", "done_fallback result=%f", result)
return result, nil
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
logHpp("GetDepresiasiTransfer", "get_project_flock_kandang_ids_error=%v", err)
return 0, err
}
logHpp("GetDepresiasiTransfer", "kandang_ids_growing=%v", kandangIDsGrowing)
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_population_error=%v", err)
return 0, err
}
logHpp("GetDepresiasiTransfer", "total_population_flock_growing=%f", totalPopulationFlockGrowing)
if totalPopulationFlockGrowing == 0 {
logHpp("GetDepresiasiTransfer", "total_population_flock_growing_zero return=0")
return 0, nil
}
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate)
if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_depresiasi_flock_growing_error=%v", err)
return 0, err
}
logHpp("GetDepresiasiTransfer", "total_depresiasi_flock_growing=%f", totalDepresiasiFlockGrowing)
result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing
logHpp("GetDepresiasiTransfer", "done result=%f", result)
return result, nil
}
func (s *hppService) getManualDepresiasiTransferFallback(projectFlockKandangId uint) (float64, error) {
logHpp("getManualDepresiasiTransferFallback", "start project_flock_kandang_id=%d", projectFlockKandangId)
projectFlockID, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_id_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "project_flock_id=%d", projectFlockID)
if projectFlockID == 0 {
logHpp("getManualDepresiasiTransferFallback", "project_flock_id_zero return=0")
return 0, nil
}
manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_manual_depreciation_cost_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "manual_cost=%f", manualCost)
if manualCost <= 0 {
logHpp("getManualDepresiasiTransferFallback", "manual_cost_non_positive return=0")
return 0, nil
}
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_kandang_ids_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "kandang_ids=%v", kandangIDs)
if len(kandangIDs) == 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_ids_empty return=0")
return 0, nil
}
totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_total_usage_qty_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty=%f", totalUsageQty)
if totalUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty_non_positive return=0")
return 0, nil
}
kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_kandang_usage_qty_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty=%f", kandangUsageQty)
if kandangUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty_non_positive return=0")
return 0, nil
}
result := manualCost * (kandangUsageQty / totalUsageQty)
logHpp("getManualDepresiasiTransferFallback", "done result=%f", result)
return result, nil
}
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
logHpp("GetHppEstimationDanRealisasi", "start total_production_cost=%f project_flock_kandang_id=%d start_date=%s end_date=%s", totalProductionCost, projectFlockKandangId, formatTimePtr(startDate), formatTimePtr(endDate))
if s.hppRepo == nil {
logHpp("GetHppEstimationDanRealisasi", "repo_nil return_empty_response")
return &HppCostResponse{}, nil
}
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_produksi_error=%v", err)
return nil, err
}
logHpp("GetHppEstimationDanRealisasi", "estim_pieces=%f estim_weight_kg=%f", estimPieces, estimWeightKg)
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_terjual_error=%v", err)
return nil, err
}
logHpp("GetHppEstimationDanRealisasi", "real_pieces=%f real_weight_kg=%f", realPieces, realWeightKg)
estimation := HppCostDetail{
Total: totalProductionCost,
@@ -403,7 +347,6 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
}
logHpp("GetHppEstimationDanRealisasi", "estimation=%+v", estimation)
real := HppCostDetail{
Total: totalProductionCost,
@@ -416,19 +359,16 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if realPieces > 0 {
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
}
logHpp("GetHppEstimationDanRealisasi", "real=%+v", real)
result := &HppCostResponse{
Estimation: estimation,
Real: real,
}
logHpp("GetHppEstimationDanRealisasi", "done response=%+v", *result)
return result, nil
}
func roundToTwoDecimals(value float64) float64 {
result := math.Round(value*100) / 100
logHpp("roundToTwoDecimals", "input=%f output=%f", value, result)
return result
}
@@ -438,7 +378,3 @@ func formatTimePtr(value *time.Time) string {
}
return value.Format(time.RFC3339)
}
func logHpp(method, format string, args ...any) {
log.Printf("[HPP][%s] "+format, append([]any{method}, args...)...)
}
@@ -48,6 +48,7 @@ type HppV2Breakdown struct {
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
ProjectFlockID uint `json:"project_flock_id"`
ProjectFlockCategory string `json:"project_flock_category,omitempty"`
HouseType string `json:"house_type,omitempty"`
KandangID uint `json:"kandang_id,omitempty"`
KandangName string `json:"kandang_name,omitempty"`
LocationID uint `json:"location_id,omitempty"`
+211 -14
View File
@@ -28,13 +28,14 @@ const (
hppV2PartManualCutover = "manual_cutover"
hppV2PartDepreciationNormal = "normal_transfer"
hppV2PartDepreciationCutover = "manual_cutover"
hppV2PartDepreciationFarmSnapshot = "farm_snapshot"
hppV2ProrationPopulation = "growing_population_share"
hppV2ProrationEggWeight = "laying_egg_weight_share"
hppV2ProrationEggPiece = "laying_egg_piece_share"
hppV2ScopePulletCost = "pullet_cost"
hppV2ScopeProductionCost = "production_cost"
hppV2CutoverFlagPakan = "PAKAN-CUTOVER"
hppV2CutoverFlagOvk = "OVK-CUTOVER"
hppV2CutoverFlagOvk = "OVK"
)
type HppV2Service interface {
@@ -115,57 +116,101 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
totalPulletCost := 0.0
totalProductionCost := 0.0
components := make([]HppV2Component, 0, 8)
appendComponent := func(component *HppV2Component) {
appendComponent := func(requestedCode string, component *HppV2Component) {
pulletBefore := totalPulletCost
productionBefore := totalProductionCost
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
utils.Log.Infof(
"HPP v2 component skipped: project_flock_kandang_id=%d period_date=%s component=%s reason=empty_or_nil total_pullet_cost=%.2f total_production_cost=%.2f",
projectFlockKandangId,
startOfDay.Format("2006-01-02"),
requestedCode,
totalPulletCost,
totalProductionCost,
)
return
}
pulletAdded := componentScopeTotal(component, hppV2ScopePulletCost)
productionAdded := componentScopeTotal(component, hppV2ScopeProductionCost)
components = append(components, *component)
totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost)
totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost)
totalPulletCost += pulletAdded
totalProductionCost += productionAdded
utils.Log.Infof(
"HPP v2 component applied: project_flock_kandang_id=%d period_date=%s component=%s component_total=%.2f pullet_added=%.2f production_added=%.2f total_pullet_before=%.2f total_pullet_after=%.2f total_production_before=%.2f total_production_after=%.2f parts_count=%d",
projectFlockKandangId,
startOfDay.Format("2006-01-02"),
component.Code,
component.Total,
pulletAdded,
productionAdded,
pulletBefore,
totalPulletCost,
productionBefore,
totalProductionCost,
len(component.Parts),
)
}
appendComponent(pakanComponent)
appendComponent(hppV2ComponentPakan, pakanComponent)
ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(ovkComponent)
appendComponent(hppV2ComponentOvk, ovkComponent)
docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(docComponent)
appendComponent(hppV2ComponentDocChickin, docComponent)
directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(directPulletComponent)
appendComponent(hppV2ComponentDirectPulletPurchase, directPulletComponent)
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(bopRegularComponent)
appendComponent(hppV2ComponentBopRegular, bopRegularComponent)
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(bopEkspedisiComponent)
appendComponent(hppV2ComponentBopEksp, bopEkspedisiComponent)
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
if err != nil {
return nil, err
}
appendComponent(manualPulletComponent)
appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent)
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, totalPulletCost)
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost)
if err != nil {
return nil, err
}
appendComponent(depreciationComponent)
depreciationCostToProduction := componentScopeTotal(depreciationComponent, hppV2ScopeProductionCost)
depreciationSource := ""
if depreciationComponent != nil && len(depreciationComponent.Parts) > 0 {
depreciationSource = depreciationComponent.Parts[0].Code
}
productionCostBeforeDepreciation := totalProductionCost
appendComponent(hppV2ComponentDepreciation, depreciationComponent)
utils.Log.Infof(
"HPP v2 depreciation cost applied: project_flock_kandang_id=%d period_date=%s depreciation_source=%s depreciation_cost=%.2f production_cost_before=%.2f production_cost_after=%.2f",
projectFlockKandangId,
startOfDay.Format("2006-01-02"),
depreciationSource,
depreciationCostToProduction,
productionCostBeforeDepreciation,
totalProductionCost,
)
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil {
@@ -179,6 +224,7 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
ProjectFlockKandangID: projectFlockKandangId,
ProjectFlockID: contextRow.ProjectFlockID,
ProjectFlockCategory: contextRow.ProjectFlockCategory,
HouseType: contextRow.HouseType,
KandangID: contextRow.KandangID,
KandangName: contextRow.KandangName,
LocationID: contextRow.LocationID,
@@ -1022,9 +1068,28 @@ func (s *hppV2Service) getDepreciationComponent(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
endDate time.Time,
totalPulletCost float64,
) (*HppV2Component, error) {
if s.hppRepo == nil || contextRow == nil || totalPulletCost <= 0 {
if s.hppRepo == nil || contextRow == nil {
return nil, nil
}
snapshotPart, err := s.buildFarmSnapshotDepreciationPart(projectFlockKandangId, contextRow, periodDate, endDate)
if err != nil {
return nil, err
}
if snapshotPart != nil {
return &HppV2Component{
Code: hppV2ComponentDepreciation,
Title: "Depreciation",
Scopes: []string{hppV2ScopeProductionCost},
Total: snapshotPart.Total,
Parts: []HppV2ComponentPart{*snapshotPart},
}, nil
}
if totalPulletCost <= 0 {
return nil, nil
}
@@ -1058,6 +1123,101 @@ func (s *hppV2Service) getDepreciationComponent(
}, nil
}
func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
endDate time.Time,
) (*HppV2ComponentPart, error) {
if contextRow == nil {
return nil, nil
}
snapshot, err := s.hppRepo.GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(context.Background(), contextRow.ProjectFlockID, periodDate)
if err != nil {
return nil, err
}
if snapshot == nil || snapshot.DepreciationValue <= 0 {
return nil, nil
}
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
if err != nil {
return nil, err
}
if len(farmPFKIDs) == 0 {
return nil, nil
}
end := endDate
targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, &end)
if err != nil {
return nil, err
}
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, &end)
if err != nil {
return nil, err
}
basis := hppV2ProrationEggWeight
numerator := targetWeight
denominator := farmWeight
if denominator <= 0 {
basis = hppV2ProrationEggPiece
numerator = targetPieces
denominator = farmPieces
}
if denominator <= 0 {
return nil, nil
}
ratio := numerator / denominator
if ratio <= 0 {
return nil, nil
}
appliedDepreciation := snapshot.DepreciationValue * ratio
if appliedDepreciation <= 0 {
return nil, nil
}
appliedPulletCostDayN := snapshot.PulletCostDayNTotal * ratio
depreciationPercent := snapshot.DepreciationPercentEffective
if appliedPulletCostDayN > 0 {
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
}
return &HppV2ComponentPart{
Code: hppV2PartDepreciationFarmSnapshot,
Title: "Farm Snapshot",
Scopes: []string{hppV2ScopeProductionCost},
Total: appliedDepreciation,
Proration: &HppV2Proration{
Basis: basis,
Numerator: numerator,
Denominator: denominator,
Ratio: ratio,
},
Details: map[string]any{
"basis_total": snapshot.DepreciationValue,
"pullet_cost_day_n": appliedPulletCostDayN,
"depreciation_percent": depreciationPercent,
"snapshot_id": snapshot.ID,
"snapshot_period_date": formatDateOnly(snapshot.PeriodDate),
"snapshot_project_flock": snapshot.ProjectFlockID,
},
References: []HppV2Reference{
{
Type: "farm_depreciation_snapshot",
ID: snapshot.ID,
Date: formatDateOnly(snapshot.PeriodDate),
Qty: 1,
Total: snapshot.DepreciationValue,
AppliedTotal: appliedDepreciation,
},
},
}, nil
}
func (s *hppV2Service) buildNormalTransferDepreciationPart(
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
transferInput *commonRepo.HppV2LatestTransferInputRow,
@@ -1211,17 +1371,40 @@ func (s *hppV2Service) buildManualCutoverDepreciationPart(
}
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
utils.Log.Infof(
"GetHppEstimationDanRealisasi started: project_flock_kandang_id=%d total_production_cost=%.2f start_date=%s end_date=%s",
projectFlockKandangId,
totalProductionCost,
formatTimePtr(startDate),
formatTimePtr(endDate),
)
if s.hppRepo == nil {
utils.Log.Warnf(
"GetHppEstimationDanRealisasi skipped: hpp repository is nil (project_flock_kandang_id=%d)",
projectFlockKandangId,
)
return &HppCostResponse{}, nil
}
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
utils.Log.WithError(err).Errorf(
"GetHppEstimationDanRealisasi failed to get estimation egg production: project_flock_kandang_id=%d end_date=%s",
projectFlockKandangId,
formatTimePtr(endDate),
)
return nil, err
}
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil {
utils.Log.WithError(err).Errorf(
"GetHppEstimationDanRealisasi failed to get realization egg sales: project_flock_kandang_id=%d start_date=%s end_date=%s",
projectFlockKandangId,
formatTimePtr(startDate),
formatTimePtr(endDate),
)
return nil, err
}
@@ -1249,6 +1432,20 @@ func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64,
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
}
utils.Log.Infof(
"GetHppEstimationDanRealisasi success: project_flock_kandang_id=%d estimation_butir=%.2f estimation_kg=%.2f estimation_harga_butir=%.2f estimation_harga_kg=%.2f real_butir=%.2f real_kg=%.2f real_harga_butir=%.2f real_harga_kg=%.2f totalProductionCost=%.2f",
projectFlockKandangId,
estimation.Butir,
estimation.Kg,
estimation.HargaButir,
estimation.HargaKg,
real.Butir,
real.Kg,
real.HargaButir,
real.HargaKg,
totalProductionCost,
)
return &HppCostResponse{
Estimation: estimation,
Real: real,
@@ -17,6 +17,7 @@ type hppV2RepoStub struct {
pfkIDsByProject map[uint][]uint
latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow
manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow
snapshotByProjectKey map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow
chickInDateByProject map[uint]*time.Time
depreciationByHouse map[string]map[int]float64
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
@@ -59,6 +60,13 @@ func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Con
return s.manualInputByProject[projectFlockID], nil
}
func (s *hppV2RepoStub) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(_ context.Context, projectFlockID uint, periodDate time.Time) (*commonRepo.HppV2FarmDepreciationSnapshotRow, error) {
if s.snapshotByProjectKey == nil {
return nil, nil
}
return s.snapshotByProjectKey[fmt.Sprintf("%d|%s", projectFlockID, periodDate.Format("2006-01-02"))], nil
}
func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) {
return s.chickInDateByProject[projectFlockID], nil
}
@@ -319,10 +327,10 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
},
},
adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
stubKey([]uint{301, 302}, []string{"OVK-CUTOVER"}): {
stubKey([]uint{301, 302}, []string{"OVK"}): {
{AdjustmentID: 8201, ProductID: 34, ProductName: "OVK Growing Cut-over", Qty: 10, Price: 10, GrandTotal: 100},
},
stubKey([]uint{30}, []string{"OVK-CUTOVER"}): {
stubKey([]uint{30}, []string{"OVK"}): {
{AdjustmentID: 8202, ProductID: 35, ProductName: "OVK Laying Cut-over", Qty: 5, Price: 10, GrandTotal: 50},
},
},
@@ -743,6 +751,82 @@ func TestHppV2CalculateHppBreakdown_AddsDepreciationForManualCutoverFromCutoverD
}
}
func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggProduction(t *testing.T) {
reportDate := mustTime(t, "2026-06-05")
repo := &hppV2RepoStub{
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
70: {
ProjectFlockKandangID: 70,
ProjectFlockID: 15,
ProjectFlockCategory: "LAYING",
KandangID: 700,
KandangName: "Kandang Snapshot",
LocationID: 25,
HouseType: "close_house",
},
},
pfkIDsByProject: map[uint][]uint{
15: {70, 71},
},
snapshotByProjectKey: map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow{
"15|2026-06-05": {
ID: 901,
ProjectFlockID: 15,
PeriodDate: reportDate,
DepreciationPercentEffective: 10,
DepreciationValue: 1000,
PulletCostDayNTotal: 10000,
},
},
eggProductionByPFK: map[uint]struct {
pieces float64
kg float64
}{
70: {pieces: 200, kg: 20},
71: {pieces: 800, kg: 80},
},
}
svc := NewHppV2Service(repo)
result, err := svc.CalculateHppBreakdown(70, &reportDate)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if result == nil {
t.Fatal("expected breakdown result")
}
var depreciation *HppV2Component
for i := range result.Components {
if result.Components[i].Code == hppV2ComponentDepreciation {
depreciation = &result.Components[i]
break
}
}
if depreciation == nil {
t.Fatal("expected depreciation component")
}
if depreciation.Total != 200 {
t.Fatalf("expected depreciation total 200, got %v", depreciation.Total)
}
if result.TotalProductionCost != 200 {
t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost)
}
if len(depreciation.Parts) != 1 {
t.Fatalf("expected one depreciation part, got %d", len(depreciation.Parts))
}
if depreciation.Parts[0].Code != hppV2PartDepreciationFarmSnapshot {
t.Fatalf("expected farm snapshot depreciation part, got %s", depreciation.Parts[0].Code)
}
if depreciation.Parts[0].Proration == nil || depreciation.Parts[0].Proration.Ratio != 0.2 {
t.Fatalf("expected proration ratio 0.2, got %+v", depreciation.Parts[0].Proration)
}
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
}
}
func stubKey(ids []uint, flags []string) string {
idParts := make([]string, 0, len(ids))
for _, id := range ids {