mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
1962 lines
59 KiB
Go
1962 lines
59 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)
|
||
// GetBopRegularProductionScopeRange / GetBopEkspedisiProductionScopeRange mengembalikan BOP
|
||
// production_cost untuk rentang [startDate, endDate] secara range-correct (tidak pernah negatif).
|
||
GetBopRegularProductionScopeRange(projectFlockKandangId uint, startDate, endDate *time.Time) (float64, error)
|
||
GetBopEkspedisiProductionScopeRange(projectFlockKandangId uint, startDate, endDate *time.Time) (float64, 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, contextRow, endDate, config, false)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if layingNormalPart != nil {
|
||
parts = append(parts, *layingNormalPart)
|
||
total += layingNormalPart.Total
|
||
}
|
||
|
||
layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, contextRow, 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,
|
||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||
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
|
||
}
|
||
|
||
// Untuk kandang LAYING, atribusi pakan/OVK berbasis kandang recording (termasuk konsumsi
|
||
// dari gudang LOKASI yang punya recording_stocks.project_flock_kandang_id = NULL). Untuk
|
||
// kandang non-laying, pertahankan semantik lama (strict rs.project_flock_kandang_id IN [pfk]).
|
||
var rows []commonRepo.HppV2UsageCostRow
|
||
var err error
|
||
if contextRow != nil && contextRow.ProjectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
||
rows, err = s.hppRepo.ListLayingUsageCostRowsByProductFlags(context.Background(), projectFlockKandangId, config.NormalFlags, endDate)
|
||
} else {
|
||
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
|
||
}
|
||
|
||
ratio, proration, err := s.layingFarmExpenseRatio(projectFlockKandangId, contextRow, endDate)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if ratio <= 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
return buildExpensePartFromRows(
|
||
rows,
|
||
hppV2PartLayingFarm,
|
||
"Laying Farm",
|
||
[]string{hppV2ScopeProductionCost},
|
||
proration,
|
||
ratio,
|
||
), nil
|
||
}
|
||
|
||
// layingFarmExpenseRatio menghitung porsi (share) kandang laying terhadap seluruh farm pada
|
||
// endDate berdasarkan bobot telur KUMULATIF (fallback ke jumlah butir bila bobot 0). Return
|
||
// ratio 0 bila tak terhitung. Diekstrak agar dipakai bersama oleh buildLayingExpenseFarmPart
|
||
// dan GetExpenseProductionScopeRange (perhitungan BOP range-correct).
|
||
func (s *hppV2Service) layingFarmExpenseRatio(
|
||
projectFlockKandangId uint,
|
||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||
endDate *time.Time,
|
||
) (float64, *HppV2Proration, error) {
|
||
if contextRow == nil {
|
||
return 0, nil, nil
|
||
}
|
||
|
||
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
|
||
if err != nil {
|
||
return 0, nil, err
|
||
}
|
||
targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
|
||
if err != nil {
|
||
return 0, nil, err
|
||
}
|
||
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate)
|
||
if err != nil {
|
||
return 0, nil, err
|
||
}
|
||
|
||
basis := hppV2ProrationEggWeight
|
||
numerator := targetWeight
|
||
denominator := farmWeight
|
||
if denominator <= 0 {
|
||
basis = hppV2ProrationEggPiece
|
||
numerator = targetPieces
|
||
denominator = farmPieces
|
||
}
|
||
if denominator <= 0 {
|
||
return 0, nil, nil
|
||
}
|
||
|
||
ratio := numerator / denominator
|
||
if ratio <= 0 {
|
||
return 0, nil, nil
|
||
}
|
||
|
||
return ratio, &HppV2Proration{
|
||
Basis: basis,
|
||
Numerator: numerator,
|
||
Denominator: denominator,
|
||
Ratio: ratio,
|
||
}, nil
|
||
}
|
||
|
||
// GetExpenseProductionScopeRange menghitung BOP production_cost satu komponen expense untuk rentang
|
||
// [startDate, endDate] secara range-correct (tidak pernah negatif untuk expense non-negatif).
|
||
// - laying-direct (ratio 1, monoton): selisih kumulatif end - start.
|
||
// - laying-farm (prorated): (expenseCum(end) - expenseCum(start)) × ratio(end).
|
||
//
|
||
// Ini mengganti pola lama di report yang men-differensiasi dua angka yang sudah diprorata dengan
|
||
// ratio berbeda (ratio(end) vs ratio(start)) — sumber bug BOP negatif saat share antar kandang bergeser.
|
||
func (s *hppV2Service) GetExpenseProductionScopeRange(projectFlockKandangId uint, startDate, endDate *time.Time, config hppV2ExpenseComponentConfig) (float64, error) {
|
||
if s.hppRepo == nil {
|
||
return 0, nil
|
||
}
|
||
|
||
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
// Samakan semantik tanggal dengan CalculateHppBreakdown: kumulatif dihitung sampai AKHIR hari
|
||
// (endOfDay). Penting karena ratio egg-weight memakai r.record_datetime (granular jam).
|
||
_, endOfEndDay, err := hppV2DayWindow(endDate)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
_, endOfStartDay, err := hppV2DayWindow(startDate)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
// laying-direct: delta kumulatif (monoton, >= 0).
|
||
directEnd, err := s.buildLayingExpenseDirectPart(projectFlockKandangId, &endOfEndDay, config)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
directStart, err := s.buildLayingExpenseDirectPart(projectFlockKandangId, &endOfStartDay, config)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
directDelta := hppV2PartTotal(directEnd) - hppV2PartTotal(directStart)
|
||
if directDelta < 0 {
|
||
directDelta = 0
|
||
}
|
||
|
||
// laying-farm: delta expense kumulatif × ratio(end).
|
||
farmRowsEnd, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), contextRow.ProjectFlockID, &endOfEndDay, config.Ekspedisi)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
farmRowsStart, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), contextRow.ProjectFlockID, &endOfStartDay, config.Ekspedisi)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
farmExpenseDelta := hppV2SumExpenseRows(farmRowsEnd) - hppV2SumExpenseRows(farmRowsStart)
|
||
if farmExpenseDelta < 0 {
|
||
farmExpenseDelta = 0
|
||
}
|
||
farmDelta := 0.0
|
||
if farmExpenseDelta > 0 {
|
||
ratio, _, err := s.layingFarmExpenseRatio(projectFlockKandangId, contextRow, &endOfEndDay)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
farmDelta = farmExpenseDelta * ratio
|
||
}
|
||
|
||
return directDelta + farmDelta, nil
|
||
}
|
||
|
||
// GetBopRegularProductionScopeRange / GetBopEkspedisiProductionScopeRange — wrapper range-correct
|
||
// untuk dua komponen BOP, memakai config yang sama dengan GetBopRegularBreakdown/GetBopEkspedisiBreakdown.
|
||
func (s *hppV2Service) GetBopRegularProductionScopeRange(projectFlockKandangId uint, startDate, endDate *time.Time) (float64, error) {
|
||
return s.GetExpenseProductionScopeRange(projectFlockKandangId, startDate, endDate, hppV2ExpenseComponentConfig{
|
||
Code: hppV2ComponentBopRegular,
|
||
Title: "BOP Regular",
|
||
Ekspedisi: false,
|
||
})
|
||
}
|
||
|
||
func (s *hppV2Service) GetBopEkspedisiProductionScopeRange(projectFlockKandangId uint, startDate, endDate *time.Time) (float64, error) {
|
||
return s.GetExpenseProductionScopeRange(projectFlockKandangId, startDate, endDate, hppV2ExpenseComponentConfig{
|
||
Code: hppV2ComponentBopEksp,
|
||
Title: "BOP Ekspedisi",
|
||
Ekspedisi: true,
|
||
})
|
||
}
|
||
|
||
func hppV2PartTotal(part *HppV2ComponentPart) float64 {
|
||
if part == nil {
|
||
return 0
|
||
}
|
||
return part.Total
|
||
}
|
||
|
||
func hppV2SumExpenseRows(rows []commonRepo.HppV2ExpenseCostRow) float64 {
|
||
total := 0.0
|
||
for _, row := range rows {
|
||
total += row.TotalCost
|
||
}
|
||
return total
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// Multi-source support: 1 target kandang bisa menerima dari MULTIPLE transfer terpisah
|
||
// (tiap transfer = 1 source kandang). Depresiasi per target = SUM dari per-transfer depresiasi.
|
||
// Setiap transfer dihitung dengan chick_in_date source-nya sendiri dan cost basis pro-rated
|
||
// berdasarkan qty share (transfer.qty / totalTransferQty).
|
||
transferInputs, err := s.hppRepo.GetAllTransferInputsByProjectFlockKandangID(context.Background(), projectFlockKandangId, periodDate)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Filter valid transfers (punya source flock id)
|
||
validTransfers := make([]commonRepo.HppV2LatestTransferInputRow, 0, len(transferInputs))
|
||
totalTransferQty := 0.0
|
||
for _, t := range transferInputs {
|
||
if t.SourceProjectFlockID == 0 {
|
||
continue
|
||
}
|
||
validTransfers = append(validTransfers, t)
|
||
totalTransferQty += t.TransferQty
|
||
}
|
||
|
||
if len(validTransfers) > 0 {
|
||
if totalPulletCost <= 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
totalDepreciation := 0.0
|
||
parts := make([]HppV2ComponentPart, 0, len(validTransfers))
|
||
for i := range validTransfers {
|
||
t := validTransfers[i]
|
||
// Pro-rate cost basis per transfer berdasarkan qty share.
|
||
// CATATAN: pendekatan ini AKURAT kalau cost per ekor sama antar source flock.
|
||
// Kalau cost per ekor berbeda signifikan antar source, follow-up: refactor
|
||
// `buildGrowingUsagePart` untuk multi-source-flock cost computation.
|
||
transferCostBasis := totalPulletCost
|
||
if totalTransferQty > 0 && len(validTransfers) > 1 {
|
||
transferCostBasis = totalPulletCost * (t.TransferQty / totalTransferQty)
|
||
}
|
||
|
||
part, partErr := s.buildNormalTransferDepreciationPart(contextRow, &t, periodDate, transferCostBasis)
|
||
if partErr != nil {
|
||
return nil, partErr
|
||
}
|
||
if part == nil {
|
||
continue
|
||
}
|
||
totalDepreciation += part.Total
|
||
parts = append(parts, *part)
|
||
}
|
||
|
||
if len(parts) == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
return &HppV2Component{
|
||
Code: hppV2ComponentDepreciation,
|
||
Title: "Depreciation",
|
||
Scopes: []string{hppV2ScopeProductionCost},
|
||
Total: totalDepreciation,
|
||
Parts: parts,
|
||
}, nil
|
||
}
|
||
|
||
// Fallback: manual cut-over (kandang tanpa transfer record)
|
||
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)
|
||
multiplicationByHouseType, effectiveDates, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, scheduleDay, contextRow.ProjectFlockID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
pulletCostDayN, depreciationValue, multiplicationPercentage := CalculateDepreciationAtDayN(
|
||
totalPulletCost,
|
||
scheduleDay,
|
||
contextRow.HouseType,
|
||
multiplicationByHouseType,
|
||
)
|
||
if depreciationValue <= 0 && pulletCostDayN <= 0 {
|
||
return nil, nil
|
||
}
|
||
totalValueAfter := pulletCostDayN * multiplicationPercentage
|
||
depreciationPercent := (1.0 - multiplicationPercentage) * 100.0
|
||
|
||
var standardEffectiveDate string
|
||
if ed, ok := effectiveDates[houseType]; ok && ed != nil {
|
||
standardEffectiveDate = formatDateOnly(*ed)
|
||
}
|
||
|
||
return &HppV2ComponentPart{
|
||
Code: hppV2PartDepreciationNormal,
|
||
Title: "Normal Transfer",
|
||
Scopes: []string{hppV2ScopeProductionCost},
|
||
Total: depreciationValue,
|
||
Details: map[string]any{
|
||
"basis_total": totalPulletCost,
|
||
"pullet_cost_day_n": pulletCostDayN,
|
||
"multiplication_percentage": multiplicationPercentage,
|
||
"total_value_pullet_after_depreciation": totalValueAfter,
|
||
"depreciation_percent": depreciationPercent,
|
||
"schedule_day": scheduleDay,
|
||
"origin_date": formatDateOnly(*originDate),
|
||
"transfer_date": formatDateOnly(transferInput.TransferDate),
|
||
"source_project_flock_id": transferInput.SourceProjectFlockID,
|
||
"standard_effective_date": standardEffectiveDate,
|
||
"kandang_population": transferInput.TransferQty,
|
||
},
|
||
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 {
|
||
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
|
||
}
|
||
|
||
populations, err := s.hppRepo.GetChickinPopulationByPFKForFarm(context.Background(), contextRow.ProjectFlockID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var totalPopulation float64
|
||
for _, qty := range populations {
|
||
totalPopulation += qty
|
||
}
|
||
kandangPopulation := populations[projectFlockKandangId]
|
||
if totalPopulation <= 0 || kandangPopulation <= 0 {
|
||
return nil, nil
|
||
}
|
||
populationShare := kandangPopulation / totalPopulation
|
||
basis := manualInput.TotalCost * populationShare
|
||
|
||
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if originDate == nil || originDate.IsZero() {
|
||
return nil, nil
|
||
}
|
||
|
||
// Hitung schedule day relatif terhadap cutover_date, bukan dari chick_in_date.
|
||
// Ini menangani kasus cut-over flock yang belum 175 hari pada period date,
|
||
// karena bisnis sudah menetapkan cutover_date sebagai awal depresiasi.
|
||
// Rumus setara secara matematis dengan DepreciationScheduleDay ketika flock >= 175 hari.
|
||
cutoverScheduleDay := DepreciationScheduleDay(*originDate, manualInput.CutoverDate, contextRow.HouseType)
|
||
startDay := 1
|
||
if cutoverScheduleDay > 0 {
|
||
startDay = cutoverScheduleDay
|
||
}
|
||
|
||
daysSinceCutover := int(dateOnly(periodDate).Sub(dateOnly(manualInput.CutoverDate)).Hours() / 24)
|
||
reportScheduleDay := startDay + daysSinceCutover
|
||
if reportScheduleDay <= 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
|
||
multiplicationByHouseType, effectiveDates, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, reportScheduleDay, contextRow.ProjectFlockID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
pulletCostDayN, depreciationValue, multiplicationPercentage := CalculateDepreciationFromDayRange(
|
||
basis,
|
||
startDay,
|
||
reportScheduleDay,
|
||
contextRow.HouseType,
|
||
multiplicationByHouseType,
|
||
)
|
||
if depreciationValue <= 0 && pulletCostDayN <= 0 {
|
||
return nil, nil
|
||
}
|
||
totalValueAfter := pulletCostDayN * multiplicationPercentage
|
||
depreciationPercent := (1.0 - multiplicationPercentage) * 100.0
|
||
_ = totalPulletCost
|
||
|
||
var standardEffectiveDate string
|
||
if ed, ok := effectiveDates[houseType]; ok && ed != nil {
|
||
standardEffectiveDate = formatDateOnly(*ed)
|
||
}
|
||
|
||
return &HppV2ComponentPart{
|
||
Code: hppV2PartDepreciationCutover,
|
||
Title: "Manual Cut-over",
|
||
Scopes: []string{hppV2ScopeProductionCost},
|
||
Total: depreciationValue,
|
||
Details: map[string]any{
|
||
"basis_total": basis,
|
||
"manual_input_total": manualInput.TotalCost,
|
||
"population_share": populationShare,
|
||
"pullet_cost_day_n": pulletCostDayN,
|
||
"multiplication_percentage": multiplicationPercentage,
|
||
"total_value_pullet_after_depreciation": totalValueAfter,
|
||
"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,
|
||
"standard_effective_date": standardEffectiveDate,
|
||
"kandang_population": kandangPopulation,
|
||
},
|
||
References: []HppV2Reference{
|
||
{
|
||
Type: "farm_depreciation_manual_input",
|
||
ID: manualInput.ID,
|
||
Date: formatDateOnly(manualInput.CutoverDate),
|
||
Qty: 1,
|
||
Total: manualInput.TotalCost,
|
||
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, time.UTC)
|
||
}
|
||
|
||
func formatDateOnly(value time.Time) string {
|
||
return dateOnly(value).Format("2006-01-02")
|
||
}
|