mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
feat: doc direct purchase cost
This commit is contained in:
@@ -59,6 +59,19 @@ type HppV2ExpenseCostRow struct {
|
|||||||
RealizationDate time.Time
|
RealizationDate time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HppV2ChickinCostRow struct {
|
||||||
|
ProjectChickinID uint
|
||||||
|
ProjectFlockKandangID uint
|
||||||
|
ChickInDate time.Time
|
||||||
|
StockableType string
|
||||||
|
StockableID uint
|
||||||
|
SourceProductID uint
|
||||||
|
SourceProductName string
|
||||||
|
Qty float64
|
||||||
|
UnitPrice float64
|
||||||
|
TotalCost float64
|
||||||
|
}
|
||||||
|
|
||||||
type HppV2CostRepository interface {
|
type HppV2CostRepository interface {
|
||||||
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
|
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
|
||||||
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
|
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
|
||||||
@@ -66,6 +79,7 @@ type HppV2CostRepository interface {
|
|||||||
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)
|
||||||
|
ListChickinCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, excludeTransferToLaying bool) ([]HppV2ChickinCostRow, error)
|
||||||
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
||||||
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
||||||
@@ -251,6 +265,181 @@ func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockKandangIDs []uint,
|
||||||
|
flagNames []string,
|
||||||
|
date *time.Time,
|
||||||
|
excludeTransferToLaying bool,
|
||||||
|
) ([]HppV2ChickinCostRow, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||||
|
return []HppV2ChickinCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
|
||||||
|
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
|
||||||
|
stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
|
||||||
|
stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String()
|
||||||
|
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
|
||||||
|
usableStockTransferOut := fifo.UsableKeyStockTransferOut.String()
|
||||||
|
|
||||||
|
rows := make([]HppV2ChickinCostRow, 0)
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select(`
|
||||||
|
pc.id AS project_chickin_id,
|
||||||
|
pc.project_flock_kandang_id AS project_flock_kandang_id,
|
||||||
|
pc.chick_in_date AS chick_in_date,
|
||||||
|
sa.stockable_type AS stockable_type,
|
||||||
|
sa.stockable_id AS stockable_id,
|
||||||
|
COALESCE(
|
||||||
|
pi.product_id,
|
||||||
|
ast_pw.product_id,
|
||||||
|
tpi.product_id,
|
||||||
|
tast_pw.product_id,
|
||||||
|
spi.product_id,
|
||||||
|
sast_pw.product_id,
|
||||||
|
0
|
||||||
|
) AS source_product_id,
|
||||||
|
COALESCE(
|
||||||
|
pi_prod.name,
|
||||||
|
ast_prod.name,
|
||||||
|
tpi_prod.name,
|
||||||
|
tast_prod.name,
|
||||||
|
spi_prod.name,
|
||||||
|
sast_prod.name,
|
||||||
|
''
|
||||||
|
) AS source_product_name,
|
||||||
|
COALESCE(SUM(sa.qty), 0) AS qty,
|
||||||
|
CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END AS unit_price,
|
||||||
|
COALESCE(SUM(sa.qty * CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END), 0) AS total_cost
|
||||||
|
`,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
stockableTransferIn,
|
||||||
|
stockableTransferToLaying,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
stockableTransferIn,
|
||||||
|
stockableTransferToLaying,
|
||||||
|
).
|
||||||
|
Joins(
|
||||||
|
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||||
|
usableProjectChickin,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeTraceChickin,
|
||||||
|
).
|
||||||
|
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").
|
||||||
|
Joins(
|
||||||
|
"LEFT JOIN stock_allocations AS tsa_transfer ON tsa_transfer.usable_type = ? AND tsa_transfer.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_transfer.status = ? AND tsa_transfer.allocation_purpose = ?",
|
||||||
|
stockableTransferToLaying,
|
||||||
|
stockableTransferToLaying,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockablePurchase).
|
||||||
|
Joins("LEFT JOIN products AS tpi_prod ON tpi_prod.id = tpi.product_id").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks AS tast ON tast.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockableAdjustment).
|
||||||
|
Joins("LEFT JOIN product_warehouses AS tast_pw ON tast_pw.id = tast.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN products AS tast_prod ON tast_prod.id = tast_pw.product_id").
|
||||||
|
Joins(
|
||||||
|
"LEFT JOIN stock_allocations AS tsa_stock ON tsa_stock.usable_type = ? AND tsa_stock.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_stock.status = ? AND tsa_stock.allocation_purpose = ?",
|
||||||
|
usableStockTransferOut,
|
||||||
|
stockableTransferIn,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Joins("LEFT JOIN purchase_items AS spi ON spi.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockablePurchase).
|
||||||
|
Joins("LEFT JOIN products AS spi_prod ON spi_prod.id = spi.product_id").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks AS sast ON sast.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockableAdjustment).
|
||||||
|
Joins("LEFT JOIN product_warehouses AS sast_pw ON sast_pw.id = sast.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN products AS sast_prod ON sast_prod.id = sast_pw.product_id").
|
||||||
|
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("pc.chick_in_date <= ?", *date).
|
||||||
|
Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = ?
|
||||||
|
AND f.flagable_id = COALESCE(
|
||||||
|
pi.product_id,
|
||||||
|
ast_pw.product_id,
|
||||||
|
tpi.product_id,
|
||||||
|
tast_pw.product_id,
|
||||||
|
spi.product_id,
|
||||||
|
sast_pw.product_id,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
AND f.name IN ?
|
||||||
|
)
|
||||||
|
`, entity.FlagableTypeProduct, flagNames)
|
||||||
|
|
||||||
|
if excludeTransferToLaying {
|
||||||
|
query = query.Where("sa.stockable_type <> ?", stockableTransferToLaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Group(`
|
||||||
|
pc.id,
|
||||||
|
pc.project_flock_kandang_id,
|
||||||
|
pc.chick_in_date,
|
||||||
|
sa.stockable_type,
|
||||||
|
sa.stockable_id,
|
||||||
|
COALESCE(
|
||||||
|
pi.product_id,
|
||||||
|
ast_pw.product_id,
|
||||||
|
tpi.product_id,
|
||||||
|
tast_pw.product_id,
|
||||||
|
spi.product_id,
|
||||||
|
sast_pw.product_id,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
COALESCE(
|
||||||
|
pi_prod.name,
|
||||||
|
ast_prod.name,
|
||||||
|
tpi_prod.name,
|
||||||
|
tast_prod.name,
|
||||||
|
spi_prod.name,
|
||||||
|
sast_prod.name,
|
||||||
|
''
|
||||||
|
),
|
||||||
|
CASE
|
||||||
|
WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0)
|
||||||
|
WHEN sa.stockable_type = '` + stockableTransferIn + `' THEN COALESCE(spi.price, sast.price, 0)
|
||||||
|
WHEN sa.stockable_type = '` + stockableTransferToLaying + `' THEN COALESCE(tpi.price, tast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
`).
|
||||||
|
Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockKandangIDs(
|
func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockKandangIDs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
projectFlockKandangIDs []uint,
|
projectFlockKandangIDs []uint,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
depreciationStartAgeDayCloseHouse = 155
|
||||||
|
depreciationStartAgeDayOpenHouse = 176
|
||||||
|
)
|
||||||
|
|
||||||
|
func NormalizeDepreciationHouseType(raw string) string {
|
||||||
|
return strings.TrimSpace(strings.ToLower(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DepreciationStartAgeDay(houseType string) int {
|
||||||
|
switch NormalizeDepreciationHouseType(houseType) {
|
||||||
|
case "close_house":
|
||||||
|
return depreciationStartAgeDayCloseHouse
|
||||||
|
case "open_house":
|
||||||
|
return depreciationStartAgeDayOpenHouse
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlockAgeDay(originDate time.Time, periodDate time.Time) int {
|
||||||
|
origin := time.Date(originDate.Year(), originDate.Month(), originDate.Day(), 0, 0, 0, 0, originDate.Location())
|
||||||
|
period := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, periodDate.Location())
|
||||||
|
if period.Before(origin) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(period.Sub(origin).Hours()/24) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func DepreciationScheduleDay(originDate time.Time, periodDate time.Time, houseType string) int {
|
||||||
|
ageDay := FlockAgeDay(originDate, periodDate)
|
||||||
|
startAgeDay := DepreciationStartAgeDay(houseType)
|
||||||
|
if ageDay <= 0 || startAgeDay <= 0 || ageDay < startAgeDay {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return ageDay - startAgeDay + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateDepreciationAtDayN(
|
||||||
|
initialPulletCost float64,
|
||||||
|
dayN int,
|
||||||
|
houseType string,
|
||||||
|
percentByHouseType map[string]map[int]float64,
|
||||||
|
) (float64, float64, float64) {
|
||||||
|
if initialPulletCost <= 0 || dayN <= 0 {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedHouseType := NormalizeDepreciationHouseType(houseType)
|
||||||
|
housePercent, exists := percentByHouseType[normalizedHouseType]
|
||||||
|
if !exists {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
current := initialPulletCost
|
||||||
|
pulletCostDayN := 0.0
|
||||||
|
depreciationValue := 0.0
|
||||||
|
depreciationPercent := 0.0
|
||||||
|
for day := 1; day <= dayN; day++ {
|
||||||
|
pct := housePercent[day]
|
||||||
|
dep := current * (pct / 100)
|
||||||
|
if day == dayN {
|
||||||
|
pulletCostDayN = current
|
||||||
|
depreciationValue = dep
|
||||||
|
depreciationPercent = pct
|
||||||
|
}
|
||||||
|
current -= dep
|
||||||
|
if current < 0 {
|
||||||
|
current = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pulletCostDayN, depreciationValue, depreciationPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN float64) float64 {
|
||||||
|
if totalPulletCostDayN <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (totalDepreciationValue / totalPulletCostDayN) * 100
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDepreciationScheduleDay_UsesHouseTypeOffsets(t *testing.T) {
|
||||||
|
openOrigin := mustDepreciationDate(t, "2026-01-01")
|
||||||
|
if got := DepreciationScheduleDay(openOrigin, mustDepreciationDate(t, "2026-06-24"), "open_house"); got != 0 {
|
||||||
|
t.Fatalf("expected open house day before start to be 0, got %d", got)
|
||||||
|
}
|
||||||
|
if got := DepreciationScheduleDay(openOrigin, mustDepreciationDate(t, "2026-06-25"), "open_house"); got != 1 {
|
||||||
|
t.Fatalf("expected open house start day to map to schedule day 1, got %d", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOrigin := mustDepreciationDate(t, "2026-01-01")
|
||||||
|
if got := DepreciationScheduleDay(closeOrigin, mustDepreciationDate(t, "2026-06-03"), "close_house"); got != 0 {
|
||||||
|
t.Fatalf("expected close house day before start to be 0, got %d", got)
|
||||||
|
}
|
||||||
|
if got := DepreciationScheduleDay(closeOrigin, mustDepreciationDate(t, "2026-06-04"), "close_house"); got != 1 {
|
||||||
|
t.Fatalf("expected close house start day to map to schedule day 1, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateDepreciationAtDayN_UsesRemainingBasisRecursively(t *testing.T) {
|
||||||
|
percentByHouseType := map[string]map[int]float64{
|
||||||
|
"close_house": {
|
||||||
|
1: 10,
|
||||||
|
2: 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationAtDayN(1000, 2, "close_house", percentByHouseType)
|
||||||
|
if pulletCostDayN != 900 {
|
||||||
|
t.Fatalf("expected remaining basis entering day 2 to be 900, got %v", pulletCostDayN)
|
||||||
|
}
|
||||||
|
if depreciationValue != 180 {
|
||||||
|
t.Fatalf("expected day 2 depreciation to be 180, got %v", depreciationValue)
|
||||||
|
}
|
||||||
|
if depreciationPercent != 20 {
|
||||||
|
t.Fatalf("expected day 2 depreciation percent to be 20, got %v", depreciationPercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDepreciationDate(t *testing.T, raw string) time.Time {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed loading timezone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := time.ParseInLocation("2006-01-02", raw, location)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed parsing date %q: %v", raw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -35,10 +35,11 @@ type HppV2ComponentPart struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HppV2Component struct {
|
type HppV2Component struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Total float64 `json:"total"`
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
Parts []HppV2ComponentPart `json:"parts"`
|
Total float64 `json:"total"`
|
||||||
|
Parts []HppV2ComponentPart `json:"parts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppV2Breakdown struct {
|
type HppV2Breakdown struct {
|
||||||
@@ -50,6 +51,7 @@ type HppV2Breakdown struct {
|
|||||||
LocationID uint `json:"location_id,omitempty"`
|
LocationID uint `json:"location_id,omitempty"`
|
||||||
PeriodDate string `json:"period_date"`
|
PeriodDate string `json:"period_date"`
|
||||||
Window HppV2DateWindow `json:"window"`
|
Window HppV2DateWindow `json:"window"`
|
||||||
|
TotalPulletCost float64 `json:"total_pullet_cost"`
|
||||||
TotalProductionCost float64 `json:"total_production_cost"`
|
TotalProductionCost float64 `json:"total_production_cost"`
|
||||||
Components []HppV2Component `json:"components"`
|
Components []HppV2Component `json:"components"`
|
||||||
Hpp HppCostResponse `json:"hpp"`
|
Hpp HppCostResponse `json:"hpp"`
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hppV2ComponentPakan = "PAKAN"
|
hppV2ComponentPakan = "PAKAN"
|
||||||
hppV2ComponentOvk = "OVK"
|
hppV2ComponentOvk = "OVK"
|
||||||
hppV2ComponentBopRegular = "BOP_REGULAR"
|
hppV2ComponentDocChickin = "DOC_CHICKIN"
|
||||||
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
|
||||||
hppV2PartGrowingNormal = "growing_normal"
|
hppV2ComponentBopRegular = "BOP_REGULAR"
|
||||||
hppV2PartGrowingCutover = "growing_cutover"
|
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
||||||
hppV2PartLayingNormal = "laying_normal"
|
hppV2PartGrowingNormal = "growing_normal"
|
||||||
hppV2PartLayingCutover = "laying_cutover"
|
hppV2PartGrowingCutover = "growing_cutover"
|
||||||
hppV2PartGrowingDirect = "growing_direct"
|
hppV2PartLayingNormal = "laying_normal"
|
||||||
hppV2PartGrowingFarm = "growing_farm"
|
hppV2PartLayingCutover = "laying_cutover"
|
||||||
hppV2PartLayingDirect = "laying_direct"
|
hppV2PartGrowingDirect = "growing_direct"
|
||||||
hppV2PartLayingFarm = "laying_farm"
|
hppV2PartGrowingFarm = "growing_farm"
|
||||||
hppV2ProrationPopulation = "growing_population_share"
|
hppV2PartLayingDirect = "laying_direct"
|
||||||
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
hppV2PartLayingFarm = "laying_farm"
|
||||||
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
hppV2ProrationPopulation = "growing_population_share"
|
||||||
hppV2CutoverFlagPakan = "PAKAN-CUTOVER"
|
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
||||||
hppV2CutoverFlagOvk = "OVK-CUTOVER"
|
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
||||||
|
hppV2ScopePulletCost = "pullet_cost"
|
||||||
|
hppV2ScopeProductionCost = "production_cost"
|
||||||
|
hppV2CutoverFlagPakan = "PAKAN-CUTOVER"
|
||||||
|
hppV2CutoverFlagOvk = "OVK-CUTOVER"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HppV2Service interface {
|
type HppV2Service interface {
|
||||||
@@ -33,10 +37,14 @@ type HppV2Service interface {
|
|||||||
CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error)
|
CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error)
|
||||||
GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
GetCostOvk(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)
|
GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetOvkBreakdown(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)
|
GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
|
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
|
||||||
@@ -99,39 +107,52 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPulletCost := 0.0
|
||||||
totalProductionCost := 0.0
|
totalProductionCost := 0.0
|
||||||
components := make([]HppV2Component, 0, 4)
|
components := make([]HppV2Component, 0, 6)
|
||||||
if pakanComponent != nil && (pakanComponent.Total > 0 || len(pakanComponent.Parts) > 0) {
|
appendComponent := func(component *HppV2Component) {
|
||||||
totalProductionCost += pakanComponent.Total
|
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
|
||||||
components = append(components, *pakanComponent)
|
return
|
||||||
|
}
|
||||||
|
components = append(components, *component)
|
||||||
|
if componentHasScope(component, hppV2ScopePulletCost) {
|
||||||
|
totalPulletCost += component.Total
|
||||||
|
}
|
||||||
|
if componentHasScope(component, hppV2ScopeProductionCost) {
|
||||||
|
totalProductionCost += component.Total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
appendComponent(pakanComponent)
|
||||||
|
|
||||||
ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
|
ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ovkComponent != nil && (ovkComponent.Total > 0 || len(ovkComponent.Parts) > 0) {
|
appendComponent(ovkComponent)
|
||||||
totalProductionCost += ovkComponent.Total
|
|
||||||
components = append(components, *ovkComponent)
|
docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
appendComponent(docComponent)
|
||||||
|
|
||||||
|
directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appendComponent(directPulletComponent)
|
||||||
|
|
||||||
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
|
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if bopRegularComponent != nil && (bopRegularComponent.Total > 0 || len(bopRegularComponent.Parts) > 0) {
|
appendComponent(bopRegularComponent)
|
||||||
totalProductionCost += bopRegularComponent.Total
|
|
||||||
components = append(components, *bopRegularComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
|
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if bopEkspedisiComponent != nil && (bopEkspedisiComponent.Total > 0 || len(bopEkspedisiComponent.Parts) > 0) {
|
appendComponent(bopEkspedisiComponent)
|
||||||
totalProductionCost += bopEkspedisiComponent.Total
|
|
||||||
components = append(components, *bopEkspedisiComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,6 +174,7 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
|||||||
Start: startOfDay.Format(time.RFC3339),
|
Start: startOfDay.Format(time.RFC3339),
|
||||||
End: endOfDay.Format(time.RFC3339),
|
End: endOfDay.Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
|
TotalPulletCost: totalPulletCost,
|
||||||
TotalProductionCost: totalProductionCost,
|
TotalProductionCost: totalProductionCost,
|
||||||
Components: components,
|
Components: components,
|
||||||
Hpp: *hppCost,
|
Hpp: *hppCost,
|
||||||
@@ -206,6 +228,88 @@ func (s *hppV2Service) GetOvkBreakdown(projectFlockKandangId uint, endDate *time
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *hppV2Service) GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
||||||
component, err := s.GetBopRegularBreakdown(projectFlockKandangId, endDate)
|
component, err := s.GetBopRegularBreakdown(projectFlockKandangId, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -249,9 +353,10 @@ func (s *hppV2Service) GetBopEkspedisiBreakdown(projectFlockKandangId uint, endD
|
|||||||
func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig) (*HppV2Component, error) {
|
func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig) (*HppV2Component, error) {
|
||||||
if s.hppRepo == nil {
|
if s.hppRepo == nil {
|
||||||
return &HppV2Component{
|
return &HppV2Component{
|
||||||
Code: config.Code,
|
Code: config.Code,
|
||||||
Title: config.Title,
|
Title: config.Title,
|
||||||
Parts: []HppV2ComponentPart{},
|
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
|
||||||
|
Parts: []HppV2ComponentPart{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,19 +405,21 @@ func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &HppV2Component{
|
return &HppV2Component{
|
||||||
Code: config.Code,
|
Code: config.Code,
|
||||||
Title: config.Title,
|
Title: config.Title,
|
||||||
Total: total,
|
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
|
||||||
Parts: parts,
|
Total: total,
|
||||||
|
Parts: parts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig) (*HppV2Component, error) {
|
func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig) (*HppV2Component, error) {
|
||||||
if s.hppRepo == nil {
|
if s.hppRepo == nil {
|
||||||
return &HppV2Component{
|
return &HppV2Component{
|
||||||
Code: config.Code,
|
Code: config.Code,
|
||||||
Title: config.Title,
|
Title: config.Title,
|
||||||
Parts: []HppV2ComponentPart{},
|
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
|
||||||
|
Parts: []HppV2ComponentPart{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,13 +468,91 @@ func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &HppV2Component{
|
return &HppV2Component{
|
||||||
Code: config.Code,
|
Code: config.Code,
|
||||||
Title: config.Title,
|
Title: config.Title,
|
||||||
Total: total,
|
Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost},
|
||||||
Parts: parts,
|
Total: total,
|
||||||
|
Parts: parts,
|
||||||
}, nil
|
}, 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,
|
||||||
|
&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, nil, 1), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) buildGrowingUsagePart(
|
func (s *hppV2Service) buildGrowingUsagePart(
|
||||||
projectFlockKandangId uint,
|
projectFlockKandangId uint,
|
||||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
@@ -825,3 +1010,58 @@ func buildExpensePartFromRows(
|
|||||||
References: references,
|
References: references,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildChickinPartFromRows(
|
||||||
|
rows []commonRepo.HppV2ChickinCostRow,
|
||||||
|
code string,
|
||||||
|
title 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,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type hppV2RepoStub struct {
|
|||||||
pfkIDsByProject map[uint][]uint
|
pfkIDsByProject map[uint][]uint
|
||||||
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
|
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
|
||||||
adjustRowsByKey map[string][]commonRepo.HppV2AdjustmentCostRow
|
adjustRowsByKey map[string][]commonRepo.HppV2AdjustmentCostRow
|
||||||
|
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
|
||||||
totalPopulationByKey map[string]float64
|
totalPopulationByKey map[string]float64
|
||||||
@@ -62,6 +63,10 @@ func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockID(_ context.Con
|
|||||||
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListChickinCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time, excludeTransferToLaying bool) ([]commonRepo.HppV2ChickinCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2ChickinCostRow{}, s.chickinRowsByKey[chickinStubKey(projectFlockKandangIDs, flagNames, excludeTransferToLaying)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2RepoStub) GetFeedUsageCost(_ context.Context, _ []uint, _ *time.Time) (float64, error) {
|
func (s *hppV2RepoStub) GetFeedUsageCost(_ context.Context, _ []uint, _ *time.Time) (float64, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@@ -339,6 +344,80 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_IncludesDocAndDirectPulletChickin(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
35: {
|
||||||
|
ProjectFlockKandangID: 35,
|
||||||
|
ProjectFlockID: 8,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 350,
|
||||||
|
KandangName: "Kandang E",
|
||||||
|
LocationID: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
9: {901, 902},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{901, 902}, nil): 1000,
|
||||||
|
},
|
||||||
|
transferSummaryByPFK: map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}{
|
||||||
|
35: {projectFlockID: 9, totalQty: 250},
|
||||||
|
},
|
||||||
|
chickinRowsByKey: map[string][]commonRepo.HppV2ChickinCostRow{
|
||||||
|
chickinStubKey([]uint{901, 902}, []string{string(utils.FlagDOC)}, false): {
|
||||||
|
{ProjectChickinID: 1, ProjectFlockKandangID: 901, ChickInDate: mustTime(t, "2026-04-01"), StockableType: "purchase_items", StockableID: 1001, SourceProductID: 77, SourceProductName: "DOC", Qty: 1000, UnitPrice: 2, TotalCost: 2000},
|
||||||
|
},
|
||||||
|
chickinStubKey([]uint{35}, []string{string(utils.FlagPullet), string(utils.FlagLayer)}, true): {
|
||||||
|
{ProjectChickinID: 2, ProjectFlockKandangID: 35, ChickInDate: mustTime(t, "2026-04-15"), StockableType: "purchase_items", StockableID: 1002, SourceProductID: 78, SourceProductName: "Pullet", Qty: 50, UnitPrice: 20, TotalCost: 1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
35: {pieces: 100, kg: 10},
|
||||||
|
},
|
||||||
|
eggSalesByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
35: {pieces: 80, kg: 8},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(35, mustDate(t, "2026-04-19"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTotals := map[string]float64{}
|
||||||
|
for _, component := range result.Components {
|
||||||
|
componentTotals[component.Code] = component.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
if componentTotals[hppV2ComponentDocChickin] != 500 {
|
||||||
|
t.Fatalf("expected doc chickin total 500, got %v", componentTotals[hppV2ComponentDocChickin])
|
||||||
|
}
|
||||||
|
if componentTotals[hppV2ComponentDirectPulletPurchase] != 1000 {
|
||||||
|
t.Fatalf("expected direct pullet purchase total 1000, got %v", componentTotals[hppV2ComponentDirectPulletPurchase])
|
||||||
|
}
|
||||||
|
if result.TotalPulletCost != 500 {
|
||||||
|
t.Fatalf("expected total pullet cost 500, got %v", result.TotalPulletCost)
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 1000 {
|
||||||
|
t.Fatalf("expected total production cost 1000, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 100 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 100, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T) {
|
func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T) {
|
||||||
repo := &hppV2RepoStub{
|
repo := &hppV2RepoStub{
|
||||||
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
@@ -471,3 +550,7 @@ func expenseStubKey(ids []uint, ekspedisi bool) string {
|
|||||||
func expenseFarmKey(projectFlockID uint, ekspedisi bool) string {
|
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 chickinStubKey(ids []uint, flags []string, excludeTransferToLaying bool) string {
|
||||||
|
return stubKey(ids, append(append([]string{}, flags...), fmt.Sprintf("exclude_transfer_to_laying=%t", excludeTransferToLaying)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -473,11 +473,11 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
|||||||
|
|
||||||
for _, row := range inputRows {
|
for _, row := range inputRows {
|
||||||
groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row)
|
groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row)
|
||||||
dayN := depreciationDayNumber(row.TransferDate, periodDate)
|
dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType))
|
||||||
if dayN > maxDay {
|
if dayN > maxDay {
|
||||||
maxDay = dayN
|
maxDay = dayN
|
||||||
}
|
}
|
||||||
houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType)))
|
houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType))
|
||||||
if houseType != "" {
|
if houseType != "" {
|
||||||
houseTypeSet[houseType] = struct{}{}
|
houseTypeSet[houseType] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -511,8 +511,8 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
|||||||
totalDepreciationValue := 0.0
|
totalDepreciationValue := 0.0
|
||||||
totalPulletCostDayN := 0.0
|
totalPulletCostDayN := 0.0
|
||||||
for _, row := range farmRows {
|
for _, row := range farmRows {
|
||||||
dayN := depreciationDayNumber(row.TransferDate, periodDate)
|
dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType))
|
||||||
houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType)))
|
houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType))
|
||||||
|
|
||||||
transferDateKey := row.TransferDate.Format("2006-01-02")
|
transferDateKey := row.TransferDate.Format("2006-01-02")
|
||||||
cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey)
|
cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey)
|
||||||
@@ -550,7 +550,7 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
|||||||
initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation
|
initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation
|
||||||
}
|
}
|
||||||
|
|
||||||
pulletCostDayN, depreciationValue, depreciationPercent := calculateDepreciationAtDayN(
|
pulletCostDayN, depreciationValue, depreciationPercent := approvalService.CalculateDepreciationAtDayN(
|
||||||
initialPulletCost,
|
initialPulletCost,
|
||||||
dayN,
|
dayN,
|
||||||
houseType,
|
houseType,
|
||||||
@@ -576,8 +576,7 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
effectivePercent := 0.0
|
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
||||||
effectivePercent = calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
|
||||||
|
|
||||||
componentsJSON, marshalErr := json.Marshal(components)
|
componentsJSON, marshalErr := json.Marshal(components)
|
||||||
if marshalErr != nil {
|
if marshalErr != nil {
|
||||||
@@ -597,57 +596,6 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func depreciationDayNumber(transferDate time.Time, periodDate time.Time) int {
|
|
||||||
transfer := time.Date(transferDate.Year(), transferDate.Month(), transferDate.Day(), 0, 0, 0, 0, transferDate.Location())
|
|
||||||
period := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, periodDate.Location())
|
|
||||||
if period.Before(transfer) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(period.Sub(transfer).Hours()/24) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateDepreciationAtDayN(
|
|
||||||
initialPulletCost float64,
|
|
||||||
dayN int,
|
|
||||||
houseType string,
|
|
||||||
percentByHouseType map[string]map[int]float64,
|
|
||||||
) (float64, float64, float64) {
|
|
||||||
if initialPulletCost <= 0 || dayN <= 0 || houseType == "" {
|
|
||||||
return 0, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
housePercent, exists := percentByHouseType[houseType]
|
|
||||||
if !exists {
|
|
||||||
return 0, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
current := initialPulletCost
|
|
||||||
pulletCostDayN := 0.0
|
|
||||||
depreciationValue := 0.0
|
|
||||||
depreciationPercent := 0.0
|
|
||||||
for day := 1; day <= dayN; day++ {
|
|
||||||
pct := housePercent[day]
|
|
||||||
dep := current * (pct / 100)
|
|
||||||
if day == dayN {
|
|
||||||
pulletCostDayN = current
|
|
||||||
depreciationValue = dep
|
|
||||||
depreciationPercent = pct
|
|
||||||
}
|
|
||||||
current -= dep
|
|
||||||
if current < 0 {
|
|
||||||
current = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pulletCostDayN, depreciationValue, depreciationPercent
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN float64) float64 {
|
|
||||||
if totalPulletCostDayN <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (totalDepreciationValue / totalPulletCostDayN) * 100
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSnapshotComponents(raw []byte) any {
|
func parseSnapshotComponents(raw []byte) any {
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return map[string]any{}
|
return map[string]any{}
|
||||||
|
|||||||
Reference in New Issue
Block a user