feat: manual pullet cost

This commit is contained in:
Adnan Zahir
2026-04-19 15:10:53 +07:00
parent a2ae139fae
commit 69d6fc165a
13 changed files with 857 additions and 33 deletions
+368 -12
View File
@@ -15,6 +15,8 @@ const (
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
hppV2ComponentBopRegular = "BOP_REGULAR"
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
hppV2ComponentDepreciation = "DEPRECIATION"
hppV2PartGrowingNormal = "growing_normal"
hppV2PartGrowingCutover = "growing_cutover"
hppV2PartLayingNormal = "laying_normal"
@@ -23,6 +25,9 @@ const (
hppV2PartGrowingFarm = "growing_farm"
hppV2PartLayingDirect = "laying_direct"
hppV2PartLayingFarm = "laying_farm"
hppV2PartManualCutover = "manual_cutover"
hppV2PartDepreciationNormal = "normal_transfer"
hppV2PartDepreciationCutover = "manual_cutover"
hppV2ProrationPopulation = "growing_population_share"
hppV2ProrationEggWeight = "laying_egg_weight_share"
hppV2ProrationEggPiece = "laying_egg_piece_share"
@@ -109,18 +114,14 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
totalPulletCost := 0.0
totalProductionCost := 0.0
components := make([]HppV2Component, 0, 6)
components := make([]HppV2Component, 0, 8)
appendComponent := func(component *HppV2Component) {
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
return
}
components = append(components, *component)
if componentHasScope(component, hppV2ScopePulletCost) {
totalPulletCost += component.Total
}
if componentHasScope(component, hppV2ScopeProductionCost) {
totalProductionCost += component.Total
}
totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost)
totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost)
}
appendComponent(pakanComponent)
@@ -154,6 +155,18 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
}
appendComponent(bopEkspedisiComponent)
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
if err != nil {
return nil, err
}
appendComponent(manualPulletComponent)
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, totalPulletCost)
if err != nil {
return nil, err
}
appendComponent(depreciationComponent)
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil {
return nil, err
@@ -527,6 +540,7 @@ func (s *hppV2Service) buildGrowingChickinPart(
rows,
partCode,
partTitle,
[]string{hppV2ScopePulletCost},
&HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: transferTotalQty,
@@ -550,7 +564,7 @@ func (s *hppV2Service) buildLayingChickinPart(
return nil, err
}
return buildChickinPartFromRows(rows, partCode, partTitle, nil, 1), nil
return buildChickinPartFromRows(rows, partCode, partTitle, []string{hppV2ScopeProductionCost}, nil, 1), nil
}
func (s *hppV2Service) buildGrowingUsagePart(
@@ -653,9 +667,10 @@ func (s *hppV2Service) buildGrowingUsagePart(
}
return &HppV2ComponentPart{
Code: partCode,
Title: partTitle,
Total: baseTotal * ratio,
Code: partCode,
Title: partTitle,
Scopes: []string{hppV2ScopePulletCost},
Total: baseTotal * ratio,
Proration: &HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: transferTotalQty,
@@ -703,6 +718,7 @@ func (s *hppV2Service) buildLayingUsagePart(
return &HppV2ComponentPart{
Code: hppV2PartLayingCutover,
Title: "Laying Cut-over",
Scopes: []string{hppV2ScopeProductionCost},
Total: total,
References: references,
}, nil
@@ -741,6 +757,7 @@ func (s *hppV2Service) buildLayingUsagePart(
return &HppV2ComponentPart{
Code: hppV2PartLayingNormal,
Title: "Laying",
Scopes: []string{hppV2ScopeProductionCost},
Total: total,
References: references,
}, nil
@@ -818,6 +835,7 @@ func (s *hppV2Service) buildGrowingExpensePart(
rows,
map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel],
map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel],
[]string{hppV2ScopePulletCost},
&HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: transferTotalQty,
@@ -838,7 +856,7 @@ func (s *hppV2Service) buildLayingExpenseDirectPart(
return nil, err
}
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", nil, 1), nil
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", []string{hppV2ScopeProductionCost}, nil, 1), nil
}
func (s *hppV2Service) buildLayingExpenseFarmPart(
@@ -893,6 +911,7 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
rows,
hppV2PartLayingFarm,
"Laying Farm",
[]string{hppV2ScopeProductionCost},
&HppV2Proration{
Basis: basis,
Numerator: numerator,
@@ -903,6 +922,294 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
), nil
}
func (s *hppV2Service) getManualPulletCostComponent(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
) (*HppV2Component, error) {
if s.hppRepo == nil || contextRow == nil {
return nil, nil
}
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil {
return nil, err
}
if sourceProjectFlockID != 0 && transferTotalQty > 0 {
return nil, nil
}
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
if err != nil {
return nil, err
}
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
return nil, nil
}
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
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
}
totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs)
if err != nil {
return nil, err
}
if totalPopulation <= 0 {
return nil, nil
}
targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
if err != nil {
return nil, err
}
if targetPopulation <= 0 {
return nil, nil
}
ratio := targetPopulation / totalPopulation
if ratio <= 0 {
return nil, nil
}
appliedTotal := manualInput.TotalCost * ratio
part := HppV2ComponentPart{
Code: hppV2PartManualCutover,
Title: "Manual Cut-over",
Scopes: []string{hppV2ScopePulletCost},
Total: appliedTotal,
Proration: &HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: targetPopulation,
Denominator: totalPopulation,
Ratio: ratio,
},
Details: map[string]any{
"cutover_date": formatDateOnly(manualInput.CutoverDate),
"farm_total_cost": manualInput.TotalCost,
"target_population": targetPopulation,
"farm_population": totalPopulation,
},
References: []HppV2Reference{
{
Type: "farm_depreciation_manual_input",
ID: manualInput.ID,
Date: formatDateOnly(manualInput.CutoverDate),
Qty: 1,
Total: manualInput.TotalCost,
AppliedTotal: appliedTotal,
},
},
}
return &HppV2Component{
Code: hppV2ComponentManualPulletCost,
Title: "Manual Pullet Cost",
Scopes: []string{hppV2ScopePulletCost},
Total: appliedTotal,
Parts: []HppV2ComponentPart{part},
}, nil
}
func (s *hppV2Service) getDepreciationComponent(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
totalPulletCost float64,
) (*HppV2Component, error) {
if s.hppRepo == nil || contextRow == nil || totalPulletCost <= 0 {
return nil, nil
}
transferInput, err := s.hppRepo.GetLatestTransferInputByProjectFlockKandangID(context.Background(), projectFlockKandangId, periodDate)
if err != nil {
return nil, err
}
var part *HppV2ComponentPart
if transferInput != nil && transferInput.SourceProjectFlockID > 0 {
part, err = s.buildNormalTransferDepreciationPart(contextRow, transferInput, periodDate, totalPulletCost)
if err != nil {
return nil, err
}
} else {
part, err = s.buildManualCutoverDepreciationPart(projectFlockKandangId, contextRow, periodDate, totalPulletCost)
if err != nil {
return nil, err
}
}
if part == nil {
return nil, nil
}
return &HppV2Component{
Code: hppV2ComponentDepreciation,
Title: "Depreciation",
Scopes: []string{hppV2ScopeProductionCost},
Total: part.Total,
Parts: []HppV2ComponentPart{*part},
}, nil
}
func (s *hppV2Service) buildNormalTransferDepreciationPart(
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
transferInput *commonRepo.HppV2LatestTransferInputRow,
periodDate time.Time,
totalPulletCost float64,
) (*HppV2ComponentPart, error) {
if contextRow == nil || transferInput == nil || totalPulletCost <= 0 {
return nil, nil
}
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), transferInput.SourceProjectFlockID)
if err != nil {
return nil, err
}
if originDate == nil || originDate.IsZero() {
return nil, nil
}
scheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
if scheduleDay <= 0 {
return nil, nil
}
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, scheduleDay)
if err != nil {
return nil, err
}
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationAtDayN(
totalPulletCost,
scheduleDay,
contextRow.HouseType,
percentByHouseType,
)
if depreciationValue <= 0 {
return nil, nil
}
return &HppV2ComponentPart{
Code: hppV2PartDepreciationNormal,
Title: "Normal Transfer",
Scopes: []string{hppV2ScopeProductionCost},
Total: depreciationValue,
Details: map[string]any{
"basis_total": totalPulletCost,
"pullet_cost_day_n": pulletCostDayN,
"depreciation_percent": depreciationPercent,
"schedule_day": scheduleDay,
"origin_date": formatDateOnly(*originDate),
"transfer_date": formatDateOnly(transferInput.TransferDate),
"source_project_flock_id": transferInput.SourceProjectFlockID,
},
References: []HppV2Reference{
{
Type: "laying_transfer",
ID: transferInput.TransferID,
Date: formatDateOnly(transferInput.TransferDate),
Qty: transferInput.TransferQty,
Total: totalPulletCost,
AppliedTotal: depreciationValue,
},
},
}, nil
}
func (s *hppV2Service) buildManualCutoverDepreciationPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
totalPulletCost float64,
) (*HppV2ComponentPart, error) {
if contextRow == nil || totalPulletCost <= 0 {
return nil, nil
}
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
if err != nil {
return nil, err
}
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
return nil, nil
}
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
return nil, nil
}
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
if err != nil {
return nil, err
}
if originDate == nil || originDate.IsZero() {
return nil, nil
}
reportScheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
if reportScheduleDay <= 0 {
return nil, nil
}
cutoverScheduleDay := DepreciationScheduleDay(*originDate, manualInput.CutoverDate, contextRow.HouseType)
startDay := 1
if cutoverScheduleDay > 0 {
startDay = cutoverScheduleDay
}
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, reportScheduleDay)
if err != nil {
return nil, err
}
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange(
totalPulletCost,
startDay,
reportScheduleDay,
contextRow.HouseType,
percentByHouseType,
)
if depreciationValue <= 0 {
return nil, nil
}
return &HppV2ComponentPart{
Code: hppV2PartDepreciationCutover,
Title: "Manual Cut-over",
Scopes: []string{hppV2ScopeProductionCost},
Total: depreciationValue,
Details: map[string]any{
"basis_total": totalPulletCost,
"pullet_cost_day_n": pulletCostDayN,
"depreciation_percent": depreciationPercent,
"schedule_day": reportScheduleDay,
"start_schedule_day": startDay,
"origin_date": formatDateOnly(*originDate),
"cutover_date": formatDateOnly(manualInput.CutoverDate),
"manual_input_id": manualInput.ID,
"project_flock_kandang": projectFlockKandangId,
},
References: []HppV2Reference{
{
Type: "farm_depreciation_manual_input",
ID: manualInput.ID,
Date: formatDateOnly(manualInput.CutoverDate),
Qty: 1,
Total: totalPulletCost,
AppliedTotal: depreciationValue,
},
},
}, nil
}
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
if s.hppRepo == nil {
return &HppCostResponse{}, nil
@@ -975,6 +1282,7 @@ func buildExpensePartFromRows(
rows []commonRepo.HppV2ExpenseCostRow,
code string,
title string,
scopes []string,
proration *HppV2Proration,
ratio float64,
) *HppV2ComponentPart {
@@ -1005,6 +1313,7 @@ func buildExpensePartFromRows(
return &HppV2ComponentPart{
Code: code,
Title: title,
Scopes: append([]string{}, scopes...),
Total: total,
Proration: proration,
References: references,
@@ -1015,6 +1324,7 @@ func buildChickinPartFromRows(
rows []commonRepo.HppV2ChickinCostRow,
code string,
title string,
scopes []string,
proration *HppV2Proration,
ratio float64,
) *HppV2ComponentPart {
@@ -1048,6 +1358,7 @@ func buildChickinPartFromRows(
return &HppV2ComponentPart{
Code: code,
Title: title,
Scopes: append([]string{}, scopes...),
Total: total,
Proration: proration,
References: references,
@@ -1065,3 +1376,48 @@ func componentHasScope(component *HppV2Component, scope string) bool {
}
return false
}
func componentScopeTotal(component *HppV2Component, scope string) float64 {
if component == nil || scope == "" {
return 0
}
total := 0.0
hasPartScopes := false
for _, part := range component.Parts {
if len(part.Scopes) == 0 {
continue
}
hasPartScopes = true
if partHasScope(&part, scope) {
total += part.Total
}
}
if hasPartScopes {
return total
}
if componentHasScope(component, scope) {
return component.Total
}
return 0
}
func partHasScope(part *HppV2ComponentPart, scope string) bool {
if part == nil || scope == "" {
return false
}
for _, candidate := range part.Scopes {
if candidate == scope {
return true
}
}
return false
}
func dateOnly(value time.Time) time.Time {
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location())
}
func formatDateOnly(value time.Time) string {
return dateOnly(value).Format("2006-01-02")
}