mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ab4e1a6ef | |||
| 217f35b250 | |||
| 61d375a59a | |||
| 09242a6998 | |||
| 7639e30326 | |||
| aa3e655a67 |
@@ -96,6 +96,7 @@ type HppV2FarmDepreciationSnapshotRow struct {
|
|||||||
DepreciationPercentEffective float64
|
DepreciationPercentEffective float64
|
||||||
DepreciationValue float64
|
DepreciationValue float64
|
||||||
PulletCostDayNTotal float64
|
PulletCostDayNTotal float64
|
||||||
|
Components []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppV2CostRepository interface {
|
type HppV2CostRepository interface {
|
||||||
@@ -114,6 +115,12 @@ type HppV2CostRepository interface {
|
|||||||
GetChickinPopulationByPFKForFarm(ctx context.Context, projectFlockID uint) (map[uint]float64, error)
|
GetChickinPopulationByPFKForFarm(ctx context.Context, projectFlockID uint) (map[uint]float64, error)
|
||||||
GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int, projectFlockID uint) (map[string]map[int]float64, map[string]*time.Time, error)
|
GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int, projectFlockID uint) (map[string]map[int]float64, map[string]*time.Time, error)
|
||||||
ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
|
ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
|
||||||
|
// ListLayingUsageCostRowsByProductFlags meng-anchor atribusi ke kandang recording
|
||||||
|
// (recordings.project_flock_kandangs_id), bukan ke recording_stocks.project_flock_kandang_id.
|
||||||
|
// Diperlukan karena pakan/OVK kandang LAYING yang dikonsumsi dari gudang tipe LOKASI
|
||||||
|
// punya recording_stocks.project_flock_kandang_id = NULL — kasus ini harus tetap diatribusikan
|
||||||
|
// ke kandang laying sebagai production_cost (bukan jatuh ke RECORDING_STOCK_ROUTE / pullet_cost).
|
||||||
|
ListLayingUsageCostRowsByProductFlags(ctx context.Context, layingProjectFlockKandangID uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
|
||||||
ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error)
|
ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error)
|
||||||
ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
||||||
ListExpenseRealizationRowsByProjectFlockID(ctx context.Context, projectFlockID uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
ListExpenseRealizationRowsByProjectFlockID(ctx context.Context, projectFlockID uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
||||||
@@ -367,18 +374,19 @@ func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlo
|
|||||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
Where("pfk_rec.project_flock_id = ?", projectFlockID).
|
Where("pfk_rec.project_flock_id = ?", projectFlockID).
|
||||||
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
|
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
|
||||||
|
// Hanya routing cross-kandang ASLI: stok yang dicatat di recording kandang X tetapi
|
||||||
|
// recording_stocks.project_flock_kandang_id menunjuk kandang lain (Y) saat ada transfer.
|
||||||
|
// Cabang lama "NOT(transferExists) AND rs.pfk IS NULL" DIHAPUS — kasus pakan/OVK laying
|
||||||
|
// dari gudang LOKASI (pfk NULL) kini diatribusikan sebagai production_cost via
|
||||||
|
// ListLayingUsageCostRowsByProductFlags, sehingga kedua jalur jadi disjoint (tanpa dobel).
|
||||||
Where(
|
Where(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)",
|
"(%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id",
|
||||||
transferExistsCondition,
|
|
||||||
transferExistsCondition,
|
transferExistsCondition,
|
||||||
),
|
),
|
||||||
periodDate,
|
periodDate,
|
||||||
string(utils.ApprovalWorkflowTransferToLaying),
|
string(utils.ApprovalWorkflowTransferToLaying),
|
||||||
entity.ApprovalActionApproved,
|
entity.ApprovalActionApproved,
|
||||||
periodDate,
|
|
||||||
string(utils.ApprovalWorkflowTransferToLaying),
|
|
||||||
entity.ApprovalActionApproved,
|
|
||||||
).
|
).
|
||||||
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
@@ -397,7 +405,7 @@ func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeri
|
|||||||
var row HppV2FarmDepreciationSnapshotRow
|
var row HppV2FarmDepreciationSnapshotRow
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Table("farm_depreciation_snapshots").
|
Table("farm_depreciation_snapshots").
|
||||||
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total").
|
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total, components").
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
Where("period_date = DATE(?)", periodDate).
|
Where("period_date = DATE(?)", periodDate).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
@@ -585,6 +593,91 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListLayingUsageCostRowsByProductFlags identik dengan ListUsageCostRowsByProductFlags,
|
||||||
|
// tetapi atribusi baris ditentukan oleh kandang RECORDING (r.project_flock_kandangs_id),
|
||||||
|
// dengan recording_stocks.project_flock_kandang_id boleh NULL (gudang LOKASI) atau sama
|
||||||
|
// dengan kandang laying. Baris yang routed ke kandang lain (rs.pfk <> kandang recording)
|
||||||
|
// SENGAJA TIDAK diikutkan di sini — itu ranah RECORDING_STOCK_ROUTE.
|
||||||
|
func (r *HppV2RepositoryImpl) ListLayingUsageCostRowsByProductFlags(
|
||||||
|
ctx context.Context,
|
||||||
|
layingProjectFlockKandangID uint,
|
||||||
|
flagNames []string,
|
||||||
|
date *time.Time,
|
||||||
|
) ([]HppV2UsageCostRow, error) {
|
||||||
|
if layingProjectFlockKandangID == 0 || len(flagNames) == 0 {
|
||||||
|
return []HppV2UsageCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
|
||||||
|
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
|
||||||
|
usableRecordingStock := fifo.UsableKeyRecordingStock.String()
|
||||||
|
|
||||||
|
rows := make([]HppV2UsageCostRow, 0)
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select(`
|
||||||
|
sa.stockable_type AS stockable_type,
|
||||||
|
sa.stockable_id AS stockable_id,
|
||||||
|
COALESCE(pi.product_id, ast_pw.product_id, 0) AS source_product_id,
|
||||||
|
COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name,
|
||||||
|
COALESCE(SUM(sa.qty), 0) AS qty,
|
||||||
|
COALESCE(MAX(CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END), 0) AS unit_price,
|
||||||
|
COALESCE(SUM(sa.qty * CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END), 0) AS total_cost,
|
||||||
|
MIN(r.record_datetime) AS first_used_at,
|
||||||
|
MAX(r.record_datetime) AS last_used_at
|
||||||
|
`,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
).
|
||||||
|
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins(
|
||||||
|
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||||
|
usableRecordingStock,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
|
Joins("LEFT JOIN products AS pi_prod ON pi_prod.id = pi.product_id").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
|
Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id").
|
||||||
|
Where("r.project_flock_kandangs_id = ?", layingProjectFlockKandangID).
|
||||||
|
Where("(rs.project_flock_kandang_id IS NULL OR rs.project_flock_kandang_id = ?)", layingProjectFlockKandangID).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("r.record_datetime <= ?", *date).
|
||||||
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames).
|
||||||
|
Group(`
|
||||||
|
sa.stockable_type,
|
||||||
|
sa.stockable_id,
|
||||||
|
COALESCE(pi.product_id, ast_pw.product_id, 0),
|
||||||
|
COALESCE(pi_prod.name, ast_prod.name, '')
|
||||||
|
`).
|
||||||
|
Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
|
func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
projectFlockKandangIDs []uint,
|
projectFlockKandangIDs []uint,
|
||||||
|
|||||||
@@ -192,19 +192,27 @@ func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t
|
|||||||
|
|
||||||
repo := &HppV2RepositoryImpl{db: db}
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
|
|
||||||
|
// Route sekarang HANYA menangkap routing cross-kandang asli
|
||||||
|
// (transferExists AND rs.pfk IS NOT NULL AND rs.pfk <> r.project_flock_kandangs_id).
|
||||||
|
// Baris pfk NULL (gudang LOKASI) tidak lagi masuk route — kini jadi production_cost
|
||||||
|
// laying-usage via ListLayingUsageCostRowsByProductFlags.
|
||||||
|
// Pada 2026-04-30 hanya rs 102 yang lolos: recording pfk 101 (transfer 1001 approved &
|
||||||
|
// executed, effective 04-05 <= 04-30), rs.pfk 201 <> 101 → 1 × 110 = 110.
|
||||||
periodDate := mustJakartaTime(t, "2026-04-30 00:00:00")
|
periodDate := mustJakartaTime(t, "2026-04-30 00:00:00")
|
||||||
total, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, periodDate)
|
total, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, periodDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
assertFloatEquals(t, total, 750)
|
assertFloatEquals(t, total, 110)
|
||||||
|
|
||||||
|
// Pada 2026-04-10 hanya recording pfk 101 & 102 yang masuk rentang tanggal; tetap hanya
|
||||||
|
// rs 102 (cross-kandang) yang lolos → 110.
|
||||||
earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
|
earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
|
||||||
earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
|
earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
assertFloatEquals(t, earlyTotal, 240)
|
assertFloatEquals(t, earlyTotal, 110)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
@@ -55,6 +56,10 @@ type HppV2Service interface {
|
|||||||
GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetBopEkspedisiBreakdown(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)
|
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +458,7 @@ func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDat
|
|||||||
total += growingCutoverPart.Total
|
total += growingCutoverPart.Total
|
||||||
}
|
}
|
||||||
|
|
||||||
layingNormalPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, false)
|
layingNormalPart, err := s.buildLayingUsagePart(projectFlockKandangId, contextRow, endDate, config, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -462,7 +467,7 @@ func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDat
|
|||||||
total += layingNormalPart.Total
|
total += layingNormalPart.Total
|
||||||
}
|
}
|
||||||
|
|
||||||
layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, true)
|
layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, contextRow, endDate, config, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -737,6 +742,7 @@ func (s *hppV2Service) buildGrowingUsagePart(
|
|||||||
|
|
||||||
func (s *hppV2Service) buildLayingUsagePart(
|
func (s *hppV2Service) buildLayingUsagePart(
|
||||||
projectFlockKandangId uint,
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
endDate *time.Time,
|
endDate *time.Time,
|
||||||
config hppV2StockComponentConfig,
|
config hppV2StockComponentConfig,
|
||||||
cutover bool,
|
cutover bool,
|
||||||
@@ -778,7 +784,16 @@ func (s *hppV2Service) buildLayingUsagePart(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.NormalFlags, endDate)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -931,17 +946,48 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
|
ratio, proration, err := s.layingFarmExpenseRatio(projectFlockKandangId, contextRow, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate)
|
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
basis := hppV2ProrationEggWeight
|
basis := hppV2ProrationEggWeight
|
||||||
@@ -953,27 +999,120 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
|||||||
denominator = farmPieces
|
denominator = farmPieces
|
||||||
}
|
}
|
||||||
if denominator <= 0 {
|
if denominator <= 0 {
|
||||||
return nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ratio := numerator / denominator
|
ratio := numerator / denominator
|
||||||
if ratio <= 0 {
|
if ratio <= 0 {
|
||||||
return nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildExpensePartFromRows(
|
return ratio, &HppV2Proration{
|
||||||
rows,
|
Basis: basis,
|
||||||
hppV2PartLayingFarm,
|
Numerator: numerator,
|
||||||
"Laying Farm",
|
Denominator: denominator,
|
||||||
[]string{hppV2ScopeProductionCost},
|
Ratio: ratio,
|
||||||
&HppV2Proration{
|
}, nil
|
||||||
Basis: basis,
|
}
|
||||||
Numerator: numerator,
|
|
||||||
Denominator: denominator,
|
// GetExpenseProductionScopeRange menghitung BOP production_cost satu komponen expense untuk rentang
|
||||||
Ratio: ratio,
|
// [startDate, endDate] secara range-correct (tidak pernah negatif untuk expense non-negatif).
|
||||||
},
|
// - laying-direct (ratio 1, monoton): selisih kumulatif end - start.
|
||||||
ratio,
|
// - laying-farm (prorated): (expenseCum(end) - expenseCum(start)) × ratio(end).
|
||||||
), nil
|
//
|
||||||
|
// 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(
|
func (s *hppV2Service) getManualPulletCostComponent(
|
||||||
@@ -1334,6 +1473,18 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
|
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
for key, value := range farmDepreciationSnapshotMetadata(snapshot.Components, projectFlockKandangId) {
|
||||||
|
details[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: hppV2PartDepreciationFarmSnapshot,
|
Code: hppV2PartDepreciationFarmSnapshot,
|
||||||
Title: "Farm Snapshot",
|
Title: "Farm Snapshot",
|
||||||
@@ -1345,14 +1496,7 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
Denominator: denominator,
|
Denominator: denominator,
|
||||||
Ratio: ratio,
|
Ratio: ratio,
|
||||||
},
|
},
|
||||||
Details: map[string]any{
|
Details: details,
|
||||||
"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{
|
References: []HppV2Reference{
|
||||||
{
|
{
|
||||||
Type: "farm_depreciation_snapshot",
|
Type: "farm_depreciation_snapshot",
|
||||||
@@ -1366,6 +1510,84 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type farmDepreciationSnapshotComponents struct {
|
||||||
|
Kandang []farmDepreciationSnapshotKandangComponent `json:"kandang"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type farmDepreciationSnapshotKandangComponent struct {
|
||||||
|
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||||
|
DayN int `json:"day_n"`
|
||||||
|
MultiplicationPercent float64 `json:"multiplication_percentage"`
|
||||||
|
ChickinDate string `json:"chickin_date"`
|
||||||
|
OriginDate string `json:"origin_date"`
|
||||||
|
StandardEffectiveDate string `json:"standard_effective_date"`
|
||||||
|
Population float64 `json:"population"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func farmDepreciationSnapshotMetadata(raw []byte, projectFlockKandangID uint) map[string]any {
|
||||||
|
result := make(map[string]any)
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var components farmDepreciationSnapshotComponents
|
||||||
|
if err := json.Unmarshal(raw, &components); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallback *farmDepreciationSnapshotKandangComponent
|
||||||
|
for i := range components.Kandang {
|
||||||
|
component := &components.Kandang[i]
|
||||||
|
if !component.hasDepreciationMetadata() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if component.ProjectFlockKandangID == projectFlockKandangID {
|
||||||
|
return component.snapshotDetails()
|
||||||
|
}
|
||||||
|
if fallback == nil {
|
||||||
|
fallback = component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fallback != nil {
|
||||||
|
return fallback.snapshotDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c farmDepreciationSnapshotKandangComponent) hasDepreciationMetadata() bool {
|
||||||
|
return c.DayN > 0 ||
|
||||||
|
c.MultiplicationPercent > 0 ||
|
||||||
|
c.ChickinDate != "" ||
|
||||||
|
c.OriginDate != "" ||
|
||||||
|
c.StandardEffectiveDate != "" ||
|
||||||
|
c.Population > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c farmDepreciationSnapshotKandangComponent) snapshotDetails() map[string]any {
|
||||||
|
chickinDate := c.ChickinDate
|
||||||
|
if chickinDate == "" {
|
||||||
|
chickinDate = c.OriginDate
|
||||||
|
}
|
||||||
|
|
||||||
|
details := map[string]any{
|
||||||
|
"schedule_day": c.DayN,
|
||||||
|
"multiplication_percentage": c.MultiplicationPercent,
|
||||||
|
}
|
||||||
|
if chickinDate != "" {
|
||||||
|
details["origin_date"] = chickinDate
|
||||||
|
details["chickin_date"] = chickinDate
|
||||||
|
}
|
||||||
|
if c.StandardEffectiveDate != "" {
|
||||||
|
details["standard_effective_date"] = c.StandardEffectiveDate
|
||||||
|
}
|
||||||
|
if c.Population > 0 {
|
||||||
|
details["kandang_population"] = c.Population
|
||||||
|
}
|
||||||
|
|
||||||
|
return details
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
||||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
||||||
|
|||||||
@@ -25,9 +25,13 @@ type hppV2RepoStub struct {
|
|||||||
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
||||||
expenseRowsByPFKKey map[string][]commonRepo.HppV2ExpenseCostRow
|
expenseRowsByPFKKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
|
expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
routeCostByProject map[uint]float64
|
// expenseRowsByFarmDateKey (opsional) membuat ListExpenseRealizationRowsByProjectFlockID
|
||||||
totalPopulationByKey map[string]float64
|
// date-aware untuk menguji perhitungan range BOP. Bila non-nil, dipakai menggantikan
|
||||||
transferSummaryByPFK map[uint]struct {
|
// expenseRowsByFarmKey; key = "<flock>|<ekspedisi>|<YYYY-MM-DD>".
|
||||||
|
expenseRowsByFarmDateKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
|
routeCostByProject map[uint]float64
|
||||||
|
totalPopulationByKey map[string]float64
|
||||||
|
transferSummaryByPFK map[uint]struct {
|
||||||
projectFlockID uint
|
projectFlockID uint
|
||||||
totalQty float64
|
totalQty float64
|
||||||
}
|
}
|
||||||
@@ -118,6 +122,10 @@ func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, proje
|
|||||||
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListLayingUsageCostRowsByProductFlags(_ context.Context, layingProjectFlockKandangID uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey([]uint{layingProjectFlockKandangID}, flagNames)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2RepoStub) ListAdjustmentCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2AdjustmentCostRow, error) {
|
func (s *hppV2RepoStub) ListAdjustmentCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2AdjustmentCostRow, error) {
|
||||||
return append([]commonRepo.HppV2AdjustmentCostRow{}, s.adjustRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
return append([]commonRepo.HppV2AdjustmentCostRow{}, s.adjustRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||||
}
|
}
|
||||||
@@ -126,7 +134,10 @@ func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockKandangIDs(_ con
|
|||||||
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByPFKKey[expenseStubKey(projectFlockKandangIDs, ekspedisi)]...), nil
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByPFKKey[expenseStubKey(projectFlockKandangIDs, ekspedisi)]...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockID(_ context.Context, projectFlockID uint, _ *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
|
func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockID(_ context.Context, projectFlockID uint, date *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
|
||||||
|
if s.expenseRowsByFarmDateKey != nil && date != nil {
|
||||||
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmDateKey[expenseFarmDateKey(projectFlockID, ekspedisi, *date)]...), nil
|
||||||
|
}
|
||||||
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,6 +825,28 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
|||||||
DepreciationPercentEffective: 10,
|
DepreciationPercentEffective: 10,
|
||||||
DepreciationValue: 1000,
|
DepreciationValue: 1000,
|
||||||
PulletCostDayNTotal: 10000,
|
PulletCostDayNTotal: 10000,
|
||||||
|
Components: []byte(`{
|
||||||
|
"kandang_count": 2,
|
||||||
|
"total_population": 1000,
|
||||||
|
"kandang": [
|
||||||
|
{
|
||||||
|
"project_flock_kandang_id": 71,
|
||||||
|
"day_n": 5,
|
||||||
|
"multiplication_percentage": 0.95,
|
||||||
|
"chickin_date": "2026-01-02",
|
||||||
|
"standard_effective_date": "2026-06-01",
|
||||||
|
"population": 800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project_flock_kandang_id": 70,
|
||||||
|
"day_n": 7,
|
||||||
|
"multiplication_percentage": 0.93,
|
||||||
|
"chickin_date": "2026-01-01",
|
||||||
|
"standard_effective_date": "2026-06-02",
|
||||||
|
"population": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
eggProductionByPFK: map[uint]struct {
|
eggProductionByPFK: map[uint]struct {
|
||||||
@@ -862,6 +895,21 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
|||||||
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
|
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
|
||||||
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
|
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
|
||||||
}
|
}
|
||||||
|
if depreciation.Parts[0].Details["schedule_day"] != 7 {
|
||||||
|
t.Fatalf("expected snapshot schedule_day 7, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["multiplication_percentage"] != 0.93 {
|
||||||
|
t.Fatalf("expected snapshot multiplication_percentage 0.93, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["chickin_date"] != "2026-01-01" {
|
||||||
|
t.Fatalf("expected snapshot chickin_date 2026-01-01, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["standard_effective_date"] != "2026-06-02" {
|
||||||
|
t.Fatalf("expected snapshot standard_effective_date 2026-06-02, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["kandang_population"] != float64(200) {
|
||||||
|
t.Fatalf("expected snapshot kandang_population 200, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stubKey(ids []uint, flags []string) string {
|
func stubKey(ids []uint, flags []string) string {
|
||||||
@@ -904,6 +952,108 @@ func expenseFarmKey(projectFlockID uint, ekspedisi bool) string {
|
|||||||
return fmt.Sprintf("farm=%d|ekspedisi=%t", projectFlockID, ekspedisi)
|
return fmt.Sprintf("farm=%d|ekspedisi=%t", projectFlockID, ekspedisi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expenseFarmDateKey(projectFlockID uint, ekspedisi bool, date time.Time) string {
|
||||||
|
return fmt.Sprintf("%d|%t|%s", projectFlockID, ekspedisi, date.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
|
||||||
func chickinStubKey(ids []uint, flags []string, excludeTransferToLaying bool) string {
|
func chickinStubKey(ids []uint, flags []string, excludeTransferToLaying bool) string {
|
||||||
return stubKey(ids, append(append([]string{}, flags...), fmt.Sprintf("exclude_transfer_to_laying=%t", excludeTransferToLaying)))
|
return stubKey(ids, append(append([]string{}, flags...), fmt.Sprintf("exclude_transfer_to_laying=%t", excludeTransferToLaying)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHppV2PakanBreakdown_LayingAttributesLokasiFeedAsProductionCost membuktikan Fix 1:
|
||||||
|
// untuk kandang LAYING, pemakaian pakan (termasuk dari gudang LOKASI dengan pfk NULL) diatribusikan
|
||||||
|
// sebagai production_cost via ListLayingUsageCostRowsByProductFlags — BUKAN pullet_cost.
|
||||||
|
// Stub memetakan ListLayingUsageCostRowsByProductFlags(50,...) ke usageRowsByKey[[50]+PAKAN].
|
||||||
|
func TestHppV2PakanBreakdown_LayingAttributesLokasiFeedAsProductionCost(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
50: {
|
||||||
|
ProjectFlockKandangID: 50,
|
||||||
|
ProjectFlockID: 20,
|
||||||
|
ProjectFlockCategory: string(utils.ProjectFlockCategoryLaying),
|
||||||
|
KandangID: 1,
|
||||||
|
LocationID: 14,
|
||||||
|
HouseType: "close_house",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||||
|
stubKey([]uint{50}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9001, SourceProductID: 9, SourceProductName: "Pakan Laying", Qty: 310, UnitPrice: 1, TotalCost: 310},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Tanpa transferSummaryByPFK[50] -> growing part nil; tanpa adjustRowsByKey -> laying cutover nil.
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
component, err := svc.GetPakanBreakdown(50, mustDate(t, "2026-05-31"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
t.Fatal("expected PAKAN component")
|
||||||
|
}
|
||||||
|
if component.Total != 310 {
|
||||||
|
t.Fatalf("expected component total 310, got %v", component.Total)
|
||||||
|
}
|
||||||
|
if len(component.Parts) != 1 || component.Parts[0].Code != hppV2PartLayingNormal {
|
||||||
|
t.Fatalf("expected single laying_normal part, got %+v", component.Parts)
|
||||||
|
}
|
||||||
|
if got := componentScopeTotal(component, hppV2ScopeProductionCost); got != 310 {
|
||||||
|
t.Fatalf("expected production_cost 310, got %v", got)
|
||||||
|
}
|
||||||
|
if got := componentScopeTotal(component, hppV2ScopePulletCost); got != 0 {
|
||||||
|
t.Fatalf("expected pullet_cost 0 (feed laying bukan pullet), got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHppV2BopProductionScopeRange_NonNegativeAndProrated membuktikan Fix 2: BOP farm-level dihitung
|
||||||
|
// sebagai (expenseCum(end) - expenseCum(start)) × ratio(end) — range-correct & tidak pernah negatif.
|
||||||
|
// Range [2026-04-30, 2026-05-31] -> engine memakai endOfDay: start=2026-05-01, end=2026-06-01.
|
||||||
|
// Share kandang 50 = 30/(30+70) = 0.3.
|
||||||
|
// - REGULAR: expense farm tumbuh 1000 -> 1300 (delta 300) => 300 × 0.3 = 90.
|
||||||
|
// - EKSPEDISI: expense farm "turun" 500 -> 200 (delta -300, kasus uji clamp) => di-clamp ke 0.
|
||||||
|
func TestHppV2BopProductionScopeRange_NonNegativeAndProrated(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
50: {ProjectFlockKandangID: 50, ProjectFlockID: 20, ProjectFlockCategory: string(utils.ProjectFlockCategoryLaying)},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
20: {50, 51},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
50: {pieces: 300, kg: 30},
|
||||||
|
51: {pieces: 700, kg: 70},
|
||||||
|
},
|
||||||
|
expenseRowsByFarmDateKey: map[string][]commonRepo.HppV2ExpenseCostRow{
|
||||||
|
// REGULAR (ekspedisi=false): kumulatif 1000 (start) -> 1300 (end)
|
||||||
|
expenseFarmDateKey(20, false, mustTime(t, "2026-05-01")): {{TotalCost: 1000}},
|
||||||
|
expenseFarmDateKey(20, false, mustTime(t, "2026-06-01")): {{TotalCost: 800}, {TotalCost: 500}},
|
||||||
|
// EKSPEDISI (ekspedisi=true): kumulatif 500 (start) -> 200 (end) => delta negatif, harus di-clamp
|
||||||
|
expenseFarmDateKey(20, true, mustTime(t, "2026-05-01")): {{TotalCost: 500}},
|
||||||
|
expenseFarmDateKey(20, true, mustTime(t, "2026-06-01")): {{TotalCost: 200}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
start := mustDate(t, "2026-04-30")
|
||||||
|
end := mustDate(t, "2026-05-31")
|
||||||
|
|
||||||
|
reg, err := svc.GetBopRegularProductionScopeRange(50, start, end)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if reg != 90 {
|
||||||
|
t.Fatalf("expected BOP regular range 90 (300 × 0.3), got %v", reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
eksp, err := svc.GetBopEkspedisiProductionScopeRange(50, start, end)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if eksp != 0 {
|
||||||
|
t.Fatalf("expected BOP ekspedisi range clamped to 0 (delta negatif), got %v", eksp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type ExpenseDepreciationV2MetaDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseDepreciationV2RowDTO struct {
|
type ExpenseDepreciationV2RowDTO struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
|
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
|
||||||
DepreciationValue float64 `json:"depreciation_value"`
|
DepreciationValue float64 `json:"depreciation_value"`
|
||||||
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
|
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
|
||||||
@@ -60,7 +60,6 @@ type ExpenseDepreciationV2RowDTO struct {
|
|||||||
TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"`
|
TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"`
|
||||||
StandardEffectiveDate string `json:"standard_effective_date,omitempty"`
|
StandardEffectiveDate string `json:"standard_effective_date,omitempty"`
|
||||||
TotalPopulation float64 `json:"total_population"`
|
TotalPopulation float64 `json:"total_population"`
|
||||||
Components any `json:"components"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
|
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpenseDepreciationRowDTOComponentsJSONContract(t *testing.T) {
|
||||||
|
v1 := ExpenseDepreciationRowDTO{
|
||||||
|
ProjectFlockID: 1,
|
||||||
|
FarmName: "Farm A",
|
||||||
|
Period: "2026-06-05",
|
||||||
|
Components: map[string]any{"kandang_count": 1},
|
||||||
|
}
|
||||||
|
rawV1, err := json.Marshal(v1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal v1 dto: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decodedV1 map[string]any
|
||||||
|
if err := json.Unmarshal(rawV1, &decodedV1); err != nil {
|
||||||
|
t.Fatalf("unmarshal v1 dto: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := decodedV1["components"]; !ok {
|
||||||
|
t.Fatalf("expected v1 components to be present, got %s", string(rawV1))
|
||||||
|
}
|
||||||
|
|
||||||
|
v2 := ExpenseDepreciationV2RowDTO{
|
||||||
|
Date: "2026-06-05",
|
||||||
|
DepreciationPercentEffective: 10,
|
||||||
|
DepreciationValue: 100,
|
||||||
|
PulletCostDayNTotal: 1000,
|
||||||
|
MultiplicationPercentage: 0.9,
|
||||||
|
DayN: 2,
|
||||||
|
ChickinDate: "2026-01-01",
|
||||||
|
TotalValuePulletAfterDepreciation: 900,
|
||||||
|
TotalPopulation: 100,
|
||||||
|
}
|
||||||
|
rawV2, err := json.Marshal(v2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal v2 dto: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decodedV2 map[string]any
|
||||||
|
if err := json.Unmarshal(rawV2, &decodedV2); err != nil {
|
||||||
|
t.Fatalf("unmarshal v2 dto: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := decodedV2["components"]; ok {
|
||||||
|
t.Fatalf("expected v2 components to be omitted, got %s", string(rawV2))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppByDeliver
|
|||||||
realizationDate = *mdp.DeliveryDate
|
realizationDate = *mdp.DeliveryDate
|
||||||
}
|
}
|
||||||
|
|
||||||
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
totalWeightKg := mdp.TotalWeight
|
||||||
salesAmount := totalWeightKg * mdp.UnitPrice
|
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||||
|
|
||||||
var hpp float64
|
var hpp float64
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *expenseDepreciationRepoMock) DeleteSnapshotsByFarmIDs(_ context.Context, farmIDs []uint) error {
|
||||||
|
m.deleteCalled = true
|
||||||
|
m.deleteFarmIDs = append([]uint{}, farmIDs...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
|
func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
|
||||||
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,7 +423,10 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
var totalDepreciationValue float64
|
var totalDepreciationValue float64
|
||||||
var totalPulletCostDayN float64
|
var totalPulletCostDayN float64
|
||||||
var totalPopulation float64
|
var totalPopulation float64
|
||||||
var allKandangComponents []depreciationKandangComponent
|
var multiplicationPercentage float64
|
||||||
|
var dayN int
|
||||||
|
var chickinDate string
|
||||||
|
var standardEffectiveDate string
|
||||||
|
|
||||||
for _, kandangID := range kandangIDs {
|
for _, kandangID := range kandangIDs {
|
||||||
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
|
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
|
||||||
@@ -444,72 +447,33 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType)
|
partPulletCostDayN := hppV2DetailFloat(part.Details, "pullet_cost_day_n")
|
||||||
component := depreciationKandangComponent{
|
partPopulation := hppV2DetailFloat(part.Details, "kandang_population")
|
||||||
ProjectFlockKandangID: breakdown.ProjectFlockKandangID,
|
partDayN := hppV2DetailInt(part.Details, "schedule_day")
|
||||||
KandangID: breakdown.KandangID,
|
partMultiplicationPercentage := hppV2DetailFloat(part.Details, "multiplication_percentage")
|
||||||
KandangName: breakdown.KandangName,
|
partChickinDate := hppV2DetailString(part.Details, "chickin_date")
|
||||||
SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"),
|
if partChickinDate == "" {
|
||||||
HouseType: houseType,
|
partChickinDate = hppV2DetailString(part.Details, "origin_date")
|
||||||
DayN: hppV2DetailInt(part.Details, "schedule_day"),
|
|
||||||
DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"),
|
|
||||||
MultiplicationPercentage: hppV2DetailFloat(part.Details, "multiplication_percentage"),
|
|
||||||
PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"),
|
|
||||||
DepreciationValue: part.Total,
|
|
||||||
TotalValuePulletAfterDepreciation: hppV2DetailFloat(part.Details, "total_value_pullet_after_depreciation"),
|
|
||||||
DepreciationSource: part.Code,
|
|
||||||
OriginDate: hppV2DetailString(part.Details, "origin_date"),
|
|
||||||
ChickinDate: hppV2DetailString(part.Details, "origin_date"),
|
|
||||||
StandardEffectiveDate: hppV2DetailString(part.Details, "standard_effective_date"),
|
|
||||||
Population: hppV2DetailFloat(part.Details, "kandang_population"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if component.HouseType == "" {
|
totalPulletCostDayN += partPulletCostDayN
|
||||||
component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type"))
|
totalDepreciationValue += part.Total
|
||||||
}
|
totalPopulation += partPopulation
|
||||||
|
|
||||||
if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil {
|
if dayN == 0 && multiplicationPercentage == 0 && chickinDate == "" &&
|
||||||
component.TransferID = ref.ID
|
(partDayN > 0 || partMultiplicationPercentage > 0 || partChickinDate != "") {
|
||||||
component.TransferDate = ref.Date
|
dayN = partDayN
|
||||||
component.TransferQty = ref.Qty
|
multiplicationPercentage = partMultiplicationPercentage
|
||||||
|
chickinDate = partChickinDate
|
||||||
|
standardEffectiveDate = hppV2DetailString(part.Details, "standard_effective_date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if part.Code == "manual_cutover" {
|
|
||||||
if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 {
|
|
||||||
component.StartScheduleDay = &startDay
|
|
||||||
}
|
|
||||||
component.CutoverDate = hppV2DetailString(part.Details, "cutover_date")
|
|
||||||
if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 {
|
|
||||||
component.ManualInputID = &manualID
|
|
||||||
}
|
|
||||||
if component.ManualInputID == nil {
|
|
||||||
if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 {
|
|
||||||
manualID := ref.ID
|
|
||||||
component.ManualInputID = &manualID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalPulletCostDayN += component.PulletCostDayN
|
|
||||||
totalDepreciationValue += component.DepreciationValue
|
|
||||||
totalPopulation += component.Population
|
|
||||||
allKandangComponents = append(allKandangComponents, component)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
||||||
|
|
||||||
components := depreciationFarmComponents{
|
|
||||||
KandangCount: len(allKandangComponents),
|
|
||||||
TotalPopulation: totalPopulation,
|
|
||||||
Kandang: allKandangComponents,
|
|
||||||
}
|
|
||||||
componentsJSON, _ := json.Marshal(components)
|
|
||||||
|
|
||||||
multiplicationPercentage, dayN, chickinDate, standardEffectiveDate := depreciationSnapshotInfo(parseSnapshotComponents(componentsJSON))
|
|
||||||
|
|
||||||
rows = append(rows, dto.ExpenseDepreciationV2RowDTO{
|
rows = append(rows, dto.ExpenseDepreciationV2RowDTO{
|
||||||
Date: dayStr,
|
Date: dayStr,
|
||||||
DepreciationPercentEffective: effectivePercent,
|
DepreciationPercentEffective: effectivePercent,
|
||||||
DepreciationValue: totalDepreciationValue,
|
DepreciationValue: totalDepreciationValue,
|
||||||
PulletCostDayNTotal: totalPulletCostDayN,
|
PulletCostDayNTotal: totalPulletCostDayN,
|
||||||
@@ -519,7 +483,6 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
|
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
|
||||||
StandardEffectiveDate: standardEffectiveDate,
|
StandardEffectiveDate: standardEffectiveDate,
|
||||||
TotalPopulation: totalPopulation,
|
TotalPopulation: totalPopulation,
|
||||||
Components: parseSnapshotComponents(componentsJSON),
|
|
||||||
})
|
})
|
||||||
actualDays++
|
actualDays++
|
||||||
}
|
}
|
||||||
@@ -3256,8 +3219,19 @@ func (s *repportService) GetHppPerFarm(ctx *fiber.Ctx) (*dto.HppPerFarmResponseD
|
|||||||
|
|
||||||
feed := codeTotals[hppPerFarmComponentPakan]
|
feed := codeTotals[hppPerFarmComponentPakan]
|
||||||
ovk := codeTotals[hppPerFarmComponentOvk]
|
ovk := codeTotals[hppPerFarmComponentOvk]
|
||||||
bop := codeTotals[hppPerFarmComponentBopRegular] + codeTotals[hppPerFarmComponentBopEkspedisi]
|
|
||||||
nonDepreciation := 0.0
|
// BOP dihitung range-correct via engine (hindari differential rasio egg-weight yang bisa
|
||||||
|
// negatif saat share antar kandang bergeser). Keluarkan kode BOP dari codeTotals agar tidak
|
||||||
|
// ikut terjumlah dua kali di akumulasi 'nonDepreciation'/'other'.
|
||||||
|
delete(codeTotals, hppPerFarmComponentBopRegular)
|
||||||
|
delete(codeTotals, hppPerFarmComponentBopEkspedisi)
|
||||||
|
|
||||||
|
bop, err := s.hppPerFarmFlockBopRange(ctx.Context(), flockID, startBreakdownDate, endBreakdownDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonDepreciation := bop
|
||||||
for _, value := range codeTotals {
|
for _, value := range codeTotals {
|
||||||
nonDepreciation += value
|
nonDepreciation += value
|
||||||
}
|
}
|
||||||
@@ -3428,6 +3402,39 @@ func (s *repportService) hppPerFarmFlockCostRange(ctx context.Context, projectFl
|
|||||||
return codeTotals, nil
|
return codeTotals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hppPerFarmFlockBopRange menjumlah BOP production_cost range-correct (BOP_REGULAR + BOP_EKSPEDISI)
|
||||||
|
// untuk seluruh PFK dalam flock, memakai GetBop*ProductionScopeRange di engine. Pendekatan ini
|
||||||
|
// menghitung delta expense kumulatif lalu memproratanya dengan rasio akhir-range — bukan
|
||||||
|
// men-differensiasi dua angka yang sudah diprorata berbeda — sehingga tidak pernah negatif.
|
||||||
|
func (s *repportService) hppPerFarmFlockBopRange(ctx context.Context, projectFlockID uint, startBreakdownDate, endBreakdownDate time.Time) (float64, error) {
|
||||||
|
if s.HppCostRepo == nil {
|
||||||
|
return 0, errors.New("hpp cost repository is not configured")
|
||||||
|
}
|
||||||
|
if s.HppV2Svc == nil {
|
||||||
|
return 0, errors.New("hpp v2 service is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
for _, pfkID := range pfkIDs {
|
||||||
|
reg, err := s.HppV2Svc.GetBopRegularProductionScopeRange(pfkID, &startBreakdownDate, &endBreakdownDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
eksp, err := s.HppV2Svc.GetBopEkspedisiProductionScopeRange(pfkID, &startBreakdownDate, &endBreakdownDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
total += reg + eksp
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
// sumHppPerFarmDepreciationOverRange sums the daily depreciation_value from
|
// sumHppPerFarmDepreciationOverRange sums the daily depreciation_value from
|
||||||
// farm_depreciation_snapshots across [startDate, endDate] per project flock,
|
// farm_depreciation_snapshots across [startDate, endDate] per project flock,
|
||||||
// computing (and persisting) any missing daily snapshot on demand — same lazy
|
// computing (and persisting) any missing daily snapshot on demand — same lazy
|
||||||
|
|||||||
Reference in New Issue
Block a user