Files
lti-api/internal/common/service/common.hppv2.service.go
T
2026-04-24 13:06:57 +07:00

1733 lines
50 KiB
Go

package service
import (
"context"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
const (
hppV2ComponentPakan = "PAKAN"
hppV2ComponentOvk = "OVK"
hppV2ComponentDocChickin = "DOC_CHICKIN"
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
hppV2ComponentBopRegular = "BOP_REGULAR"
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
hppV2ComponentRecordingStockRoute = "RECORDING_STOCK_ROUTE"
hppV2ComponentDepreciation = "DEPRECIATION"
hppV2PartGrowingNormal = "growing_normal"
hppV2PartGrowingCutover = "growing_cutover"
hppV2PartLayingNormal = "laying_normal"
hppV2PartLayingCutover = "laying_cutover"
hppV2PartGrowingDirect = "growing_direct"
hppV2PartGrowingFarm = "growing_farm"
hppV2PartLayingDirect = "laying_direct"
hppV2PartLayingFarm = "laying_farm"
hppV2PartManualCutover = "manual_cutover"
hppV2PartRecordingStockRoute = "recording_stock_route"
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 = string(utils.FlagPakan)
hppV2CutoverFlagOvk = "OVK"
)
type HppV2Service interface {
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error)
GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetCostDocChickin(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetCostDirectPulletPurchase(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetDocChickinBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
}
type hppV2Service struct {
hppRepo commonRepo.HppV2CostRepository
}
type hppV2StockComponentConfig struct {
Code string
Title string
NormalFlags []string
CutoverFlags []string
}
type hppV2ExpenseComponentConfig struct {
Code string
Title string
Ekspedisi bool
}
func NewHppV2Service(hppRepo commonRepo.HppV2CostRepository) HppV2Service {
return &hppV2Service{hppRepo: hppRepo}
}
func (s *hppV2Service) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
breakdown, err := s.CalculateHppBreakdown(projectFlockKandangId, date)
if err != nil {
return nil, err
}
if breakdown == nil {
return &HppCostResponse{}, nil
}
result := breakdown.Hpp
return &result, nil
}
func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error) {
if s.hppRepo == nil {
return &HppV2Breakdown{
ProjectFlockKandangID: projectFlockKandangId,
Hpp: HppCostResponse{},
}, nil
}
startOfDay, endOfDay, err := hppV2DayWindow(date)
if err != nil {
return nil, err
}
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
if err != nil {
return nil, err
}
pakanComponent, err := s.GetPakanBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
totalPulletCost := 0.0
totalProductionCost := 0.0
components := make([]HppV2Component, 0, 8)
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 += 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(hppV2ComponentPakan, pakanComponent)
ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentOvk, ovkComponent)
docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentDocChickin, docComponent)
directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentDirectPulletPurchase, directPulletComponent)
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentBopRegular, bopRegularComponent)
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentBopEksp, bopEkspedisiComponent)
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent)
recordingStockRouteComponent, err := s.getRecordingStockRouteComponent(projectFlockKandangId, contextRow, startOfDay)
if err != nil {
return nil, err
}
appendComponent(hppV2ComponentRecordingStockRoute, recordingStockRouteComponent)
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost)
if err != nil {
return nil, err
}
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 {
return nil, err
}
if hppCost == nil {
hppCost = &HppCostResponse{}
}
return &HppV2Breakdown{
ProjectFlockKandangID: projectFlockKandangId,
ProjectFlockID: contextRow.ProjectFlockID,
ProjectFlockCategory: contextRow.ProjectFlockCategory,
HouseType: contextRow.HouseType,
KandangID: contextRow.KandangID,
KandangName: contextRow.KandangName,
LocationID: contextRow.LocationID,
PeriodDate: startOfDay.Format("2006-01-02"),
Window: HppV2DateWindow{
Start: startOfDay.Format(time.RFC3339),
End: endOfDay.Format(time.RFC3339),
},
TotalPulletCost: totalPulletCost,
TotalProductionCost: totalProductionCost,
Components: components,
Hpp: *hppCost,
}, nil
}
func (s *hppV2Service) GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetPakanBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{
Code: hppV2ComponentPakan,
Title: "Pakan",
NormalFlags: []string{string(utils.FlagPakan)},
CutoverFlags: []string{hppV2CutoverFlagPakan},
})
}
func (s *hppV2Service) GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetOvkBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{
Code: hppV2ComponentOvk,
Title: "OVK",
NormalFlags: []string{
string(utils.FlagOVK),
string(utils.FlagObat),
string(utils.FlagVitamin),
string(utils.FlagKimia),
},
CutoverFlags: []string{hppV2CutoverFlagOvk},
})
}
func (s *hppV2Service) GetCostDocChickin(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetDocChickinBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetCostDirectPulletPurchase(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetDocChickinBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
if s.hppRepo == nil {
return &HppV2Component{
Code: hppV2ComponentDocChickin,
Title: "DOC Chick-in",
Scopes: []string{hppV2ScopePulletCost},
Parts: []HppV2ComponentPart{},
}, nil
}
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
if err != nil {
return nil, err
}
part, err := s.buildGrowingChickinPart(projectFlockKandangId, contextRow, endDate, []string{string(utils.FlagDOC)}, false, hppV2PartGrowingDirect, "Growing DOC")
if err != nil {
return nil, err
}
parts := make([]HppV2ComponentPart, 0, 1)
total := 0.0
if part != nil {
parts = append(parts, *part)
total += part.Total
}
return &HppV2Component{
Code: hppV2ComponentDocChickin,
Title: "DOC Chick-in",
Scopes: []string{hppV2ScopePulletCost},
Total: total,
Parts: parts,
}, nil
}
func (s *hppV2Service) GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
part, err := s.buildLayingChickinPart(projectFlockKandangId, endDate, []string{string(utils.FlagPullet), string(utils.FlagLayer)}, true, hppV2PartLayingDirect, "Laying Direct Pullet")
if err != nil {
return nil, err
}
parts := make([]HppV2ComponentPart, 0, 1)
total := 0.0
if part != nil {
parts = append(parts, *part)
total += part.Total
}
return &HppV2Component{
Code: hppV2ComponentDirectPulletPurchase,
Title: "Direct Pullet Purchase",
Scopes: []string{hppV2ScopeProductionCost},
Total: total,
Parts: parts,
}, nil
}
func (s *hppV2Service) GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetBopRegularBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
component, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
if component == nil {
return 0, nil
}
return component.Total, nil
}
func (s *hppV2Service) GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{
Code: hppV2ComponentBopRegular,
Title: "BOP Regular",
Ekspedisi: false,
})
}
func (s *hppV2Service) GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{
Code: hppV2ComponentBopEksp,
Title: "BOP Ekspedisi",
Ekspedisi: true,
})
}
func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig) (*HppV2Component, error) {
if s.hppRepo == nil {
return &HppV2Component{
Code: config.Code,
Title: config.Title,
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
Parts: []HppV2ComponentPart{},
}, nil
}
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
if err != nil {
return nil, err
}
parts := make([]HppV2ComponentPart, 0, 4)
total := 0.0
growingPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, false)
if err != nil {
return nil, err
}
if growingPart != nil {
parts = append(parts, *growingPart)
total += growingPart.Total
}
growingCutoverPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, true)
if err != nil {
return nil, err
}
if growingCutoverPart != nil {
parts = append(parts, *growingCutoverPart)
total += growingCutoverPart.Total
}
layingNormalPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, false)
if err != nil {
return nil, err
}
if layingNormalPart != nil {
parts = append(parts, *layingNormalPart)
total += layingNormalPart.Total
}
layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, true)
if err != nil {
return nil, err
}
if layingCutoverPart != nil {
parts = append(parts, *layingCutoverPart)
total += layingCutoverPart.Total
}
return &HppV2Component{
Code: config.Code,
Title: config.Title,
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
Total: total,
Parts: parts,
}, nil
}
func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig) (*HppV2Component, error) {
if s.hppRepo == nil {
return &HppV2Component{
Code: config.Code,
Title: config.Title,
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
Parts: []HppV2ComponentPart{},
}, nil
}
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
if err != nil {
return nil, err
}
parts := make([]HppV2ComponentPart, 0, 4)
total := 0.0
growingDirect, err := s.buildGrowingExpenseDirectPart(projectFlockKandangId, contextRow, endDate, config)
if err != nil {
return nil, err
}
if growingDirect != nil {
parts = append(parts, *growingDirect)
total += growingDirect.Total
}
growingFarm, err := s.buildGrowingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config)
if err != nil {
return nil, err
}
if growingFarm != nil {
parts = append(parts, *growingFarm)
total += growingFarm.Total
}
layingDirect, err := s.buildLayingExpenseDirectPart(projectFlockKandangId, endDate, config)
if err != nil {
return nil, err
}
if layingDirect != nil {
parts = append(parts, *layingDirect)
total += layingDirect.Total
}
layingFarm, err := s.buildLayingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config)
if err != nil {
return nil, err
}
if layingFarm != nil {
parts = append(parts, *layingFarm)
total += layingFarm.Total
}
return &HppV2Component{
Code: config.Code,
Title: config.Title,
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
Total: total,
Parts: parts,
}, nil
}
func (s *hppV2Service) buildGrowingChickinPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
flagNames []string,
excludeTransferToLaying bool,
partCode string,
partTitle string,
) (*HppV2ComponentPart, error) {
if 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
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return nil, err
}
if len(kandangIDsGrowing) == 0 {
return nil, nil
}
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
return nil, err
}
if totalPopulationFlockGrowing <= 0 {
return nil, nil
}
ratio := transferTotalQty / totalPopulationFlockGrowing
if ratio <= 0 {
return nil, nil
}
rows, err := s.hppRepo.ListChickinCostRowsByProductFlags(context.Background(), kandangIDsGrowing, flagNames, endDate, excludeTransferToLaying)
if err != nil {
return nil, err
}
return buildChickinPartFromRows(
rows,
partCode,
partTitle,
[]string{hppV2ScopePulletCost},
&HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: transferTotalQty,
Denominator: totalPopulationFlockGrowing,
Ratio: ratio,
},
ratio,
), nil
}
func (s *hppV2Service) buildLayingChickinPart(
projectFlockKandangId uint,
endDate *time.Time,
flagNames []string,
excludeTransferToLaying bool,
partCode string,
partTitle string,
) (*HppV2ComponentPart, error) {
rows, err := s.hppRepo.ListChickinCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, flagNames, endDate, excludeTransferToLaying)
if err != nil {
return nil, err
}
return buildChickinPartFromRows(rows, partCode, partTitle, []string{hppV2ScopeProductionCost}, nil, 1), nil
}
func (s *hppV2Service) buildGrowingUsagePart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
config hppV2StockComponentConfig,
cutover bool,
) (*HppV2ComponentPart, error) {
if 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
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return nil, err
}
if len(kandangIDsGrowing) == 0 {
return nil, nil
}
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
return nil, err
}
if totalPopulationFlockGrowing == 0 {
return nil, nil
}
ratio := transferTotalQty / totalPopulationFlockGrowing
if ratio <= 0 {
return nil, nil
}
partCode := hppV2PartGrowingNormal
partTitle := "Growing"
baseRows := make([]HppV2Reference, 0)
baseTotal := 0.0
if cutover {
partCode = hppV2PartGrowingCutover
partTitle = "Growing Cut-over"
rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.CutoverFlags, endDate)
if err != nil {
return nil, err
}
for _, row := range rows {
rowTotal := adjustmentRowTotalCost(row)
baseTotal += rowTotal
baseRows = append(baseRows, HppV2Reference{
Type: "adjustment_stock",
ID: row.AdjustmentID,
ProjectFlockKandangID: row.ProjectFlockKandangID,
ProductID: row.ProductID,
ProductName: row.ProductName,
Date: row.CreatedAt.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.Price,
Total: rowTotal,
AppliedTotal: rowTotal * ratio,
})
}
} else {
rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.NormalFlags, endDate)
if err != nil {
return nil, err
}
for _, row := range rows {
baseTotal += row.TotalCost
refDate := row.LastUsedAt
if refDate.IsZero() {
refDate = row.FirstUsedAt
}
baseRows = append(baseRows, HppV2Reference{
Type: "stock_allocation",
ID: row.StockableID,
StockableType: row.StockableType,
ProductID: row.SourceProductID,
ProductName: row.SourceProductName,
Date: refDate.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.UnitPrice,
Total: row.TotalCost,
AppliedTotal: row.TotalCost * ratio,
})
}
}
if baseTotal == 0 {
return nil, nil
}
return &HppV2ComponentPart{
Code: partCode,
Title: partTitle,
Scopes: []string{hppV2ScopePulletCost},
Total: baseTotal * ratio,
Proration: &HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: transferTotalQty,
Denominator: totalPopulationFlockGrowing,
Ratio: ratio,
},
References: baseRows,
}, nil
}
func (s *hppV2Service) buildLayingUsagePart(
projectFlockKandangId uint,
endDate *time.Time,
config hppV2StockComponentConfig,
cutover bool,
) (*HppV2ComponentPart, error) {
if cutover {
rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.CutoverFlags, endDate)
if err != nil {
return nil, err
}
total := 0.0
references := make([]HppV2Reference, 0, len(rows))
for _, row := range rows {
rowTotal := adjustmentRowTotalCost(row)
total += rowTotal
references = append(references, HppV2Reference{
Type: "adjustment_stock",
ID: row.AdjustmentID,
ProjectFlockKandangID: row.ProjectFlockKandangID,
ProductID: row.ProductID,
ProductName: row.ProductName,
Date: row.CreatedAt.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.Price,
Total: rowTotal,
AppliedTotal: rowTotal,
})
}
if total == 0 {
return nil, nil
}
return &HppV2ComponentPart{
Code: hppV2PartLayingCutover,
Title: "Laying Cut-over",
Scopes: []string{hppV2ScopeProductionCost},
Total: total,
References: references,
}, nil
}
rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.NormalFlags, endDate)
if err != nil {
return nil, err
}
total := 0.0
references := make([]HppV2Reference, 0, len(rows))
for _, row := range rows {
total += row.TotalCost
refDate := row.LastUsedAt
if refDate.IsZero() {
refDate = row.FirstUsedAt
}
references = append(references, HppV2Reference{
Type: "stock_allocation",
ID: row.StockableID,
StockableType: row.StockableType,
ProductID: row.SourceProductID,
ProductName: row.SourceProductName,
Date: refDate.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.UnitPrice,
Total: row.TotalCost,
AppliedTotal: row.TotalCost,
})
}
if total == 0 {
return nil, nil
}
return &HppV2ComponentPart{
Code: hppV2PartLayingNormal,
Title: "Laying",
Scopes: []string{hppV2ScopeProductionCost},
Total: total,
References: references,
}, nil
}
func (s *hppV2Service) buildGrowingExpenseDirectPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
config hppV2ExpenseComponentConfig,
) (*HppV2ComponentPart, error) {
return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, false)
}
func (s *hppV2Service) buildGrowingExpenseFarmPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
config hppV2ExpenseComponentConfig,
) (*HppV2ComponentPart, error) {
return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, true)
}
func (s *hppV2Service) buildGrowingExpensePart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
config hppV2ExpenseComponentConfig,
farmLevel bool,
) (*HppV2ComponentPart, error) {
if 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
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return nil, err
}
if len(kandangIDsGrowing) == 0 {
return nil, nil
}
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
return nil, err
}
if totalPopulationFlockGrowing <= 0 {
return nil, nil
}
ratio := transferTotalQty / totalPopulationFlockGrowing
if ratio <= 0 {
return nil, nil
}
var rows []commonRepo.HppV2ExpenseCostRow
if farmLevel {
rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), sourceProjectFlockID, endDate, config.Ekspedisi)
} else {
rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), kandangIDsGrowing, endDate, config.Ekspedisi)
}
if err != nil {
return nil, err
}
return buildExpensePartFromRows(
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,
Denominator: totalPopulationFlockGrowing,
Ratio: ratio,
},
ratio,
), nil
}
func (s *hppV2Service) buildLayingExpenseDirectPart(
projectFlockKandangId uint,
endDate *time.Time,
config hppV2ExpenseComponentConfig,
) (*HppV2ComponentPart, error) {
rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), []uint{projectFlockKandangId}, endDate, config.Ekspedisi)
if err != nil {
return nil, err
}
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", []string{hppV2ScopeProductionCost}, nil, 1), nil
}
func (s *hppV2Service) buildLayingExpenseFarmPart(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
endDate *time.Time,
config hppV2ExpenseComponentConfig,
) (*HppV2ComponentPart, error) {
if contextRow == nil {
return nil, nil
}
rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), contextRow.ProjectFlockID, endDate, config.Ekspedisi)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
if err != nil {
return nil, err
}
targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return nil, err
}
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate)
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
}
return buildExpensePartFromRows(
rows,
hppV2PartLayingFarm,
"Laying Farm",
[]string{hppV2ScopeProductionCost},
&HppV2Proration{
Basis: basis,
Numerator: numerator,
Denominator: denominator,
Ratio: ratio,
},
ratio,
), 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) getRecordingStockRouteComponent(
projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time,
) (*HppV2Component, error) {
if s.hppRepo == nil || contextRow == nil || periodDate.IsZero() {
return nil, nil
}
farmTotalCost, err := s.hppRepo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(
context.Background(),
contextRow.ProjectFlockID,
periodDate,
)
if err != nil {
return nil, err
}
if farmTotalCost <= 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
}
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 := farmTotalCost * ratio
if appliedTotal <= 0 {
return nil, nil
}
part := HppV2ComponentPart{
Code: hppV2PartRecordingStockRoute,
Title: "Recording Stock Route",
Scopes: []string{hppV2ScopePulletCost},
Total: appliedTotal,
Proration: &HppV2Proration{
Basis: hppV2ProrationPopulation,
Numerator: targetPopulation,
Denominator: totalPopulation,
Ratio: ratio,
},
Details: map[string]any{
"period_date": formatDateOnly(periodDate),
"farm_total_cost": farmTotalCost,
"target_population": targetPopulation,
"farm_population": totalPopulation,
"project_flock_id": contextRow.ProjectFlockID,
"project_flock_kandang_id": projectFlockKandangId,
},
References: []HppV2Reference{
{
Type: "recording_stock_route",
Date: formatDateOnly(periodDate),
Qty: 1,
Total: farmTotalCost,
AppliedTotal: appliedTotal,
},
},
}
return &HppV2Component{
Code: hppV2ComponentRecordingStockRoute,
Title: "Recording Stock Route",
Scopes: []string{hppV2ScopePulletCost},
Total: appliedTotal,
Parts: []HppV2ComponentPart{part},
}, nil
}
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 {
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
}
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) 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,
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) {
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
}
recordingQty, recordingWeight, adjustmentQty, adjustmentWeight, err := s.hppRepo.GetEggProduksiBreakdownByProjectFlockKandangIds(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
}
estimPieces := recordingQty + adjustmentQty
estimWeightKg := recordingWeight + adjustmentWeight
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
}
estimation := HppCostDetail{
Total: totalProductionCost,
Kg: estimWeightKg,
Butir: estimPieces,
}
if estimWeightKg > 0 {
estimation.HargaKg = roundToTwoDecimals(totalProductionCost / estimWeightKg)
}
if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
}
real := HppCostDetail{
Total: totalProductionCost,
Kg: realWeightKg,
Butir: realPieces,
}
if realWeightKg > 0 {
real.HargaKg = roundToTwoDecimals(totalProductionCost / realWeightKg)
}
if realPieces > 0 {
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,
DebugValues: &HppCostDebugValues{
RecordingEggQty: recordingQty,
RecordingEggWeight: recordingWeight,
AdjustmentEggQty: adjustmentQty,
AdjustmentEggWeight: adjustmentWeight,
SoldEggQty: realPieces,
SoldEggWeight: realWeightKg,
},
}, nil
}
func hppV2DayWindow(date *time.Time) (time.Time, time.Time, error) {
if date == nil {
now := time.Now()
date = &now
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return time.Time{}, time.Time{}, err
}
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour)
return startOfDay, endOfDay, nil
}
func adjustmentRowTotalCost(row commonRepo.HppV2AdjustmentCostRow) float64 {
if row.GrandTotal > 0 {
return row.GrandTotal
}
return row.Qty * row.Price
}
func buildExpensePartFromRows(
rows []commonRepo.HppV2ExpenseCostRow,
code string,
title string,
scopes []string,
proration *HppV2Proration,
ratio float64,
) *HppV2ComponentPart {
if len(rows) == 0 {
return nil
}
total := 0.0
references := make([]HppV2Reference, 0, len(rows))
for _, row := range rows {
total += row.TotalCost * ratio
references = append(references, HppV2Reference{
Type: "expense_realization",
ID: row.ExpenseRealizationID,
ProductID: row.NonstockID,
ProductName: row.NonstockName,
Date: row.RealizationDate.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.Price,
Total: row.TotalCost,
AppliedTotal: row.TotalCost * ratio,
})
}
if total == 0 {
return nil
}
return &HppV2ComponentPart{
Code: code,
Title: title,
Scopes: append([]string{}, scopes...),
Total: total,
Proration: proration,
References: references,
}
}
func buildChickinPartFromRows(
rows []commonRepo.HppV2ChickinCostRow,
code string,
title string,
scopes []string,
proration *HppV2Proration,
ratio float64,
) *HppV2ComponentPart {
if len(rows) == 0 {
return nil
}
total := 0.0
references := make([]HppV2Reference, 0, len(rows))
for _, row := range rows {
total += row.TotalCost * ratio
projectFlockKandangID := row.ProjectFlockKandangID
references = append(references, HppV2Reference{
Type: "project_chickin",
ID: row.ProjectChickinID,
StockableType: row.StockableType,
ProjectFlockKandangID: &projectFlockKandangID,
ProductID: row.SourceProductID,
ProductName: row.SourceProductName,
Date: row.ChickInDate.Format("2006-01-02"),
Qty: row.Qty,
UnitPrice: row.UnitPrice,
Total: row.TotalCost,
AppliedTotal: row.TotalCost * ratio,
})
}
if total == 0 {
return nil
}
return &HppV2ComponentPart{
Code: code,
Title: title,
Scopes: append([]string{}, scopes...),
Total: total,
Proration: proration,
References: references,
}
}
func componentHasScope(component *HppV2Component, scope string) bool {
if component == nil || scope == "" {
return false
}
for _, candidate := range component.Scopes {
if candidate == scope {
return true
}
}
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")
}