mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
feat: manual pullet cost
This commit is contained in:
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -72,9 +73,29 @@ type HppV2ChickinCostRow struct {
|
|||||||
TotalCost float64
|
TotalCost float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HppV2LatestTransferInputRow struct {
|
||||||
|
ProjectFlockKandangID uint
|
||||||
|
SourceProjectFlockID uint
|
||||||
|
TransferDate time.Time
|
||||||
|
TransferQty float64
|
||||||
|
TransferID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2ManualDepreciationInputRow struct {
|
||||||
|
ID uint
|
||||||
|
ProjectFlockID uint
|
||||||
|
TotalCost float64
|
||||||
|
CutoverDate time.Time
|
||||||
|
Note *string
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error)
|
||||||
|
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error)
|
||||||
|
GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
|
||||||
|
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, 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)
|
||||||
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)
|
||||||
@@ -136,6 +157,149 @@ func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, pro
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetLatestTransferInputByProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
period time.Time,
|
||||||
|
) (*HppV2LatestTransferInputRow, error) {
|
||||||
|
var row HppV2LatestTransferInputRow
|
||||||
|
query := `
|
||||||
|
WITH latest_transfer_approval AS (
|
||||||
|
SELECT a.approvable_id, a.action
|
||||||
|
FROM approvals a
|
||||||
|
JOIN (
|
||||||
|
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||||
|
FROM approvals
|
||||||
|
WHERE approvable_type = @approval_type
|
||||||
|
GROUP BY approvable_id
|
||||||
|
) la
|
||||||
|
ON la.approvable_id = a.approvable_id
|
||||||
|
AND la.latest_action_at = a.action_at
|
||||||
|
WHERE a.approvable_type = @approval_type
|
||||||
|
),
|
||||||
|
approved_transfers AS (
|
||||||
|
SELECT
|
||||||
|
lt.id,
|
||||||
|
lt.from_project_flock_id,
|
||||||
|
COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date
|
||||||
|
FROM laying_transfers lt
|
||||||
|
JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id
|
||||||
|
WHERE lt.deleted_at IS NULL
|
||||||
|
AND lt.executed_at IS NOT NULL
|
||||||
|
AND lta.action = 'APPROVED'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
|
||||||
|
at.from_project_flock_id AS source_project_flock_id,
|
||||||
|
at.effective_date AS transfer_date,
|
||||||
|
ltt.total_qty AS transfer_qty,
|
||||||
|
at.id AS transfer_id
|
||||||
|
FROM laying_transfer_targets ltt
|
||||||
|
JOIN approved_transfers at ON at.id = ltt.laying_transfer_id
|
||||||
|
WHERE ltt.deleted_at IS NULL
|
||||||
|
AND ltt.target_project_flock_kandang_id = @project_flock_kandang_id
|
||||||
|
AND at.effective_date <= DATE(@period_date)
|
||||||
|
ORDER BY at.effective_date DESC, at.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Raw(query, map[string]any{
|
||||||
|
"approval_type": utils.ApprovalWorkflowTransferToLaying.String(),
|
||||||
|
"project_flock_kandang_id": projectFlockKandangId,
|
||||||
|
"period_date": period,
|
||||||
|
}).Scan(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if row.TransferID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockID uint,
|
||||||
|
) (*HppV2ManualDepreciationInputRow, error) {
|
||||||
|
var row HppV2ManualDepreciationInputRow
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("farm_depreciation_manual_inputs").
|
||||||
|
Select("id, project_flock_id, total_cost, cutover_date, note").
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Limit(1).
|
||||||
|
Take(&row).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) {
|
||||||
|
type row struct {
|
||||||
|
ChickInDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected row
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_chickins AS pc").
|
||||||
|
Select("MIN(pc.chick_in_date) AS chick_in_date").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
|
||||||
|
Where("pc.deleted_at IS NULL").
|
||||||
|
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||||
|
Scan(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if selected.ChickInDate == nil || selected.ChickInDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected.ChickInDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetDepreciationPercents(
|
||||||
|
ctx context.Context,
|
||||||
|
houseTypes []string,
|
||||||
|
maxDay int,
|
||||||
|
) (map[string]map[int]float64, error) {
|
||||||
|
result := make(map[string]map[int]float64)
|
||||||
|
if len(houseTypes) == 0 || maxDay <= 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
HouseType string
|
||||||
|
Day int
|
||||||
|
DepreciationPercent float64
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]row, 0)
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("house_depreciation_standards").
|
||||||
|
Select("house_type::text AS house_type, day, depreciation_percent").
|
||||||
|
Where("house_type::text IN ?", houseTypes).
|
||||||
|
Where("day <= ?", maxDay).
|
||||||
|
Order("house_type ASC, day ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range rows {
|
||||||
|
if _, exists := result[item.HouseType]; !exists {
|
||||||
|
result[item.HouseType] = make(map[int]float64)
|
||||||
|
}
|
||||||
|
result[item.HouseType][item.Day] = item.DepreciationPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
projectFlockKandangIDs []uint,
|
projectFlockKandangIDs []uint,
|
||||||
|
|||||||
@@ -49,7 +49,23 @@ func CalculateDepreciationAtDayN(
|
|||||||
houseType string,
|
houseType string,
|
||||||
percentByHouseType map[string]map[int]float64,
|
percentByHouseType map[string]map[int]float64,
|
||||||
) (float64, float64, float64) {
|
) (float64, float64, float64) {
|
||||||
if initialPulletCost <= 0 || dayN <= 0 {
|
return CalculateDepreciationFromDayRange(initialPulletCost, 1, dayN, houseType, percentByHouseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateDepreciationFromDayRange(
|
||||||
|
initialPulletCost float64,
|
||||||
|
startDay int,
|
||||||
|
endDay int,
|
||||||
|
houseType string,
|
||||||
|
percentByHouseType map[string]map[int]float64,
|
||||||
|
) (float64, float64, float64) {
|
||||||
|
if initialPulletCost <= 0 || endDay <= 0 {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
if startDay <= 0 {
|
||||||
|
startDay = 1
|
||||||
|
}
|
||||||
|
if endDay < startDay {
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +79,10 @@ func CalculateDepreciationAtDayN(
|
|||||||
pulletCostDayN := 0.0
|
pulletCostDayN := 0.0
|
||||||
depreciationValue := 0.0
|
depreciationValue := 0.0
|
||||||
depreciationPercent := 0.0
|
depreciationPercent := 0.0
|
||||||
for day := 1; day <= dayN; day++ {
|
for day := startDay; day <= endDay; day++ {
|
||||||
pct := housePercent[day]
|
pct := housePercent[day]
|
||||||
dep := current * (pct / 100)
|
dep := current * (pct / 100)
|
||||||
if day == dayN {
|
if day == endDay {
|
||||||
pulletCostDayN = current
|
pulletCostDayN = current
|
||||||
depreciationValue = dep
|
depreciationValue = dep
|
||||||
depreciationPercent = pct
|
depreciationPercent = pct
|
||||||
|
|||||||
@@ -43,6 +43,27 @@ func TestCalculateDepreciationAtDayN_UsesRemainingBasisRecursively(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalculateDepreciationFromDayRange_StartsFromProvidedScheduleDay(t *testing.T) {
|
||||||
|
percentByHouseType := map[string]map[int]float64{
|
||||||
|
"close_house": {
|
||||||
|
1: 10,
|
||||||
|
2: 20,
|
||||||
|
3: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange(1000, 2, 3, "close_house", percentByHouseType)
|
||||||
|
if pulletCostDayN != 800 {
|
||||||
|
t.Fatalf("expected remaining basis entering day 3 to be 800, got %v", pulletCostDayN)
|
||||||
|
}
|
||||||
|
if depreciationValue != 40 {
|
||||||
|
t.Fatalf("expected day 3 depreciation to be 40, got %v", depreciationValue)
|
||||||
|
}
|
||||||
|
if depreciationPercent != 5 {
|
||||||
|
t.Fatalf("expected day 3 depreciation percent to be 5, got %v", depreciationPercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustDepreciationDate(t *testing.T, raw string) time.Time {
|
func mustDepreciationDate(t *testing.T, raw string) time.Time {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ type HppV2Reference struct {
|
|||||||
type HppV2ComponentPart struct {
|
type HppV2ComponentPart struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
Total float64 `json:"total"`
|
Total float64 `json:"total"`
|
||||||
Proration *HppV2Proration `json:"proration,omitempty"`
|
Proration *HppV2Proration `json:"proration,omitempty"`
|
||||||
|
Details map[string]any `json:"details,omitempty"`
|
||||||
References []HppV2Reference `json:"references,omitempty"`
|
References []HppV2Reference `json:"references,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const (
|
|||||||
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
|
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
|
||||||
hppV2ComponentBopRegular = "BOP_REGULAR"
|
hppV2ComponentBopRegular = "BOP_REGULAR"
|
||||||
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
||||||
|
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
|
||||||
|
hppV2ComponentDepreciation = "DEPRECIATION"
|
||||||
hppV2PartGrowingNormal = "growing_normal"
|
hppV2PartGrowingNormal = "growing_normal"
|
||||||
hppV2PartGrowingCutover = "growing_cutover"
|
hppV2PartGrowingCutover = "growing_cutover"
|
||||||
hppV2PartLayingNormal = "laying_normal"
|
hppV2PartLayingNormal = "laying_normal"
|
||||||
@@ -23,6 +25,9 @@ const (
|
|||||||
hppV2PartGrowingFarm = "growing_farm"
|
hppV2PartGrowingFarm = "growing_farm"
|
||||||
hppV2PartLayingDirect = "laying_direct"
|
hppV2PartLayingDirect = "laying_direct"
|
||||||
hppV2PartLayingFarm = "laying_farm"
|
hppV2PartLayingFarm = "laying_farm"
|
||||||
|
hppV2PartManualCutover = "manual_cutover"
|
||||||
|
hppV2PartDepreciationNormal = "normal_transfer"
|
||||||
|
hppV2PartDepreciationCutover = "manual_cutover"
|
||||||
hppV2ProrationPopulation = "growing_population_share"
|
hppV2ProrationPopulation = "growing_population_share"
|
||||||
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
||||||
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
||||||
@@ -109,18 +114,14 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
|||||||
|
|
||||||
totalPulletCost := 0.0
|
totalPulletCost := 0.0
|
||||||
totalProductionCost := 0.0
|
totalProductionCost := 0.0
|
||||||
components := make([]HppV2Component, 0, 6)
|
components := make([]HppV2Component, 0, 8)
|
||||||
appendComponent := func(component *HppV2Component) {
|
appendComponent := func(component *HppV2Component) {
|
||||||
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
|
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
components = append(components, *component)
|
components = append(components, *component)
|
||||||
if componentHasScope(component, hppV2ScopePulletCost) {
|
totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost)
|
||||||
totalPulletCost += component.Total
|
totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost)
|
||||||
}
|
|
||||||
if componentHasScope(component, hppV2ScopeProductionCost) {
|
|
||||||
totalProductionCost += component.Total
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
appendComponent(pakanComponent)
|
appendComponent(pakanComponent)
|
||||||
|
|
||||||
@@ -154,6 +155,18 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
|||||||
}
|
}
|
||||||
appendComponent(bopEkspedisiComponent)
|
appendComponent(bopEkspedisiComponent)
|
||||||
|
|
||||||
|
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appendComponent(manualPulletComponent)
|
||||||
|
|
||||||
|
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, totalPulletCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appendComponent(depreciationComponent)
|
||||||
|
|
||||||
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -527,6 +540,7 @@ func (s *hppV2Service) buildGrowingChickinPart(
|
|||||||
rows,
|
rows,
|
||||||
partCode,
|
partCode,
|
||||||
partTitle,
|
partTitle,
|
||||||
|
[]string{hppV2ScopePulletCost},
|
||||||
&HppV2Proration{
|
&HppV2Proration{
|
||||||
Basis: hppV2ProrationPopulation,
|
Basis: hppV2ProrationPopulation,
|
||||||
Numerator: transferTotalQty,
|
Numerator: transferTotalQty,
|
||||||
@@ -550,7 +564,7 @@ func (s *hppV2Service) buildLayingChickinPart(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildChickinPartFromRows(rows, partCode, partTitle, nil, 1), nil
|
return buildChickinPartFromRows(rows, partCode, partTitle, []string{hppV2ScopeProductionCost}, nil, 1), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) buildGrowingUsagePart(
|
func (s *hppV2Service) buildGrowingUsagePart(
|
||||||
@@ -653,9 +667,10 @@ func (s *hppV2Service) buildGrowingUsagePart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: partCode,
|
Code: partCode,
|
||||||
Title: partTitle,
|
Title: partTitle,
|
||||||
Total: baseTotal * ratio,
|
Scopes: []string{hppV2ScopePulletCost},
|
||||||
|
Total: baseTotal * ratio,
|
||||||
Proration: &HppV2Proration{
|
Proration: &HppV2Proration{
|
||||||
Basis: hppV2ProrationPopulation,
|
Basis: hppV2ProrationPopulation,
|
||||||
Numerator: transferTotalQty,
|
Numerator: transferTotalQty,
|
||||||
@@ -703,6 +718,7 @@ func (s *hppV2Service) buildLayingUsagePart(
|
|||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: hppV2PartLayingCutover,
|
Code: hppV2PartLayingCutover,
|
||||||
Title: "Laying Cut-over",
|
Title: "Laying Cut-over",
|
||||||
|
Scopes: []string{hppV2ScopeProductionCost},
|
||||||
Total: total,
|
Total: total,
|
||||||
References: references,
|
References: references,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -741,6 +757,7 @@ func (s *hppV2Service) buildLayingUsagePart(
|
|||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: hppV2PartLayingNormal,
|
Code: hppV2PartLayingNormal,
|
||||||
Title: "Laying",
|
Title: "Laying",
|
||||||
|
Scopes: []string{hppV2ScopeProductionCost},
|
||||||
Total: total,
|
Total: total,
|
||||||
References: references,
|
References: references,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -818,6 +835,7 @@ func (s *hppV2Service) buildGrowingExpensePart(
|
|||||||
rows,
|
rows,
|
||||||
map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel],
|
map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel],
|
||||||
map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel],
|
map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel],
|
||||||
|
[]string{hppV2ScopePulletCost},
|
||||||
&HppV2Proration{
|
&HppV2Proration{
|
||||||
Basis: hppV2ProrationPopulation,
|
Basis: hppV2ProrationPopulation,
|
||||||
Numerator: transferTotalQty,
|
Numerator: transferTotalQty,
|
||||||
@@ -838,7 +856,7 @@ func (s *hppV2Service) buildLayingExpenseDirectPart(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", nil, 1), nil
|
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", []string{hppV2ScopeProductionCost}, nil, 1), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) buildLayingExpenseFarmPart(
|
func (s *hppV2Service) buildLayingExpenseFarmPart(
|
||||||
@@ -893,6 +911,7 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
|||||||
rows,
|
rows,
|
||||||
hppV2PartLayingFarm,
|
hppV2PartLayingFarm,
|
||||||
"Laying Farm",
|
"Laying Farm",
|
||||||
|
[]string{hppV2ScopeProductionCost},
|
||||||
&HppV2Proration{
|
&HppV2Proration{
|
||||||
Basis: basis,
|
Basis: basis,
|
||||||
Numerator: numerator,
|
Numerator: numerator,
|
||||||
@@ -903,6 +922,294 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
|||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) getManualPulletCostComponent(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
periodDate time.Time,
|
||||||
|
) (*HppV2Component, error) {
|
||||||
|
if s.hppRepo == nil || contextRow == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if sourceProjectFlockID != 0 && transferTotalQty > 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(farmPFKIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if totalPopulation <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if targetPopulation <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := targetPopulation / totalPopulation
|
||||||
|
if ratio <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appliedTotal := manualInput.TotalCost * ratio
|
||||||
|
part := HppV2ComponentPart{
|
||||||
|
Code: hppV2PartManualCutover,
|
||||||
|
Title: "Manual Cut-over",
|
||||||
|
Scopes: []string{hppV2ScopePulletCost},
|
||||||
|
Total: appliedTotal,
|
||||||
|
Proration: &HppV2Proration{
|
||||||
|
Basis: hppV2ProrationPopulation,
|
||||||
|
Numerator: targetPopulation,
|
||||||
|
Denominator: totalPopulation,
|
||||||
|
Ratio: ratio,
|
||||||
|
},
|
||||||
|
Details: map[string]any{
|
||||||
|
"cutover_date": formatDateOnly(manualInput.CutoverDate),
|
||||||
|
"farm_total_cost": manualInput.TotalCost,
|
||||||
|
"target_population": targetPopulation,
|
||||||
|
"farm_population": totalPopulation,
|
||||||
|
},
|
||||||
|
References: []HppV2Reference{
|
||||||
|
{
|
||||||
|
Type: "farm_depreciation_manual_input",
|
||||||
|
ID: manualInput.ID,
|
||||||
|
Date: formatDateOnly(manualInput.CutoverDate),
|
||||||
|
Qty: 1,
|
||||||
|
Total: manualInput.TotalCost,
|
||||||
|
AppliedTotal: appliedTotal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: hppV2ComponentManualPulletCost,
|
||||||
|
Title: "Manual Pullet Cost",
|
||||||
|
Scopes: []string{hppV2ScopePulletCost},
|
||||||
|
Total: appliedTotal,
|
||||||
|
Parts: []HppV2ComponentPart{part},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) getDepreciationComponent(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
periodDate time.Time,
|
||||||
|
totalPulletCost float64,
|
||||||
|
) (*HppV2Component, error) {
|
||||||
|
if s.hppRepo == nil || contextRow == nil || totalPulletCost <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transferInput, err := s.hppRepo.GetLatestTransferInputByProjectFlockKandangID(context.Background(), projectFlockKandangId, periodDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var part *HppV2ComponentPart
|
||||||
|
if transferInput != nil && transferInput.SourceProjectFlockID > 0 {
|
||||||
|
part, err = s.buildNormalTransferDepreciationPart(contextRow, transferInput, periodDate, totalPulletCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
part, err = s.buildManualCutoverDepreciationPart(projectFlockKandangId, contextRow, periodDate, totalPulletCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if part == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: hppV2ComponentDepreciation,
|
||||||
|
Title: "Depreciation",
|
||||||
|
Scopes: []string{hppV2ScopeProductionCost},
|
||||||
|
Total: part.Total,
|
||||||
|
Parts: []HppV2ComponentPart{*part},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
||||||
|
periodDate time.Time,
|
||||||
|
totalPulletCost float64,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if contextRow == nil || transferInput == nil || totalPulletCost <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), transferInput.SourceProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if originDate == nil || originDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
|
||||||
|
if scheduleDay <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
|
||||||
|
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, scheduleDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationAtDayN(
|
||||||
|
totalPulletCost,
|
||||||
|
scheduleDay,
|
||||||
|
contextRow.HouseType,
|
||||||
|
percentByHouseType,
|
||||||
|
)
|
||||||
|
if depreciationValue <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: hppV2PartDepreciationNormal,
|
||||||
|
Title: "Normal Transfer",
|
||||||
|
Scopes: []string{hppV2ScopeProductionCost},
|
||||||
|
Total: depreciationValue,
|
||||||
|
Details: map[string]any{
|
||||||
|
"basis_total": totalPulletCost,
|
||||||
|
"pullet_cost_day_n": pulletCostDayN,
|
||||||
|
"depreciation_percent": depreciationPercent,
|
||||||
|
"schedule_day": scheduleDay,
|
||||||
|
"origin_date": formatDateOnly(*originDate),
|
||||||
|
"transfer_date": formatDateOnly(transferInput.TransferDate),
|
||||||
|
"source_project_flock_id": transferInput.SourceProjectFlockID,
|
||||||
|
},
|
||||||
|
References: []HppV2Reference{
|
||||||
|
{
|
||||||
|
Type: "laying_transfer",
|
||||||
|
ID: transferInput.TransferID,
|
||||||
|
Date: formatDateOnly(transferInput.TransferDate),
|
||||||
|
Qty: transferInput.TransferQty,
|
||||||
|
Total: totalPulletCost,
|
||||||
|
AppliedTotal: depreciationValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildManualCutoverDepreciationPart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
periodDate time.Time,
|
||||||
|
totalPulletCost float64,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if contextRow == nil || totalPulletCost <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if originDate == nil || originDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reportScheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
|
||||||
|
if reportScheduleDay <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoverScheduleDay := DepreciationScheduleDay(*originDate, manualInput.CutoverDate, contextRow.HouseType)
|
||||||
|
startDay := 1
|
||||||
|
if cutoverScheduleDay > 0 {
|
||||||
|
startDay = cutoverScheduleDay
|
||||||
|
}
|
||||||
|
|
||||||
|
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
|
||||||
|
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, reportScheduleDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange(
|
||||||
|
totalPulletCost,
|
||||||
|
startDay,
|
||||||
|
reportScheduleDay,
|
||||||
|
contextRow.HouseType,
|
||||||
|
percentByHouseType,
|
||||||
|
)
|
||||||
|
if depreciationValue <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: hppV2PartDepreciationCutover,
|
||||||
|
Title: "Manual Cut-over",
|
||||||
|
Scopes: []string{hppV2ScopeProductionCost},
|
||||||
|
Total: depreciationValue,
|
||||||
|
Details: map[string]any{
|
||||||
|
"basis_total": totalPulletCost,
|
||||||
|
"pullet_cost_day_n": pulletCostDayN,
|
||||||
|
"depreciation_percent": depreciationPercent,
|
||||||
|
"schedule_day": reportScheduleDay,
|
||||||
|
"start_schedule_day": startDay,
|
||||||
|
"origin_date": formatDateOnly(*originDate),
|
||||||
|
"cutover_date": formatDateOnly(manualInput.CutoverDate),
|
||||||
|
"manual_input_id": manualInput.ID,
|
||||||
|
"project_flock_kandang": projectFlockKandangId,
|
||||||
|
},
|
||||||
|
References: []HppV2Reference{
|
||||||
|
{
|
||||||
|
Type: "farm_depreciation_manual_input",
|
||||||
|
ID: manualInput.ID,
|
||||||
|
Date: formatDateOnly(manualInput.CutoverDate),
|
||||||
|
Qty: 1,
|
||||||
|
Total: totalPulletCost,
|
||||||
|
AppliedTotal: depreciationValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
|
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
|
||||||
if s.hppRepo == nil {
|
if s.hppRepo == nil {
|
||||||
return &HppCostResponse{}, nil
|
return &HppCostResponse{}, nil
|
||||||
@@ -975,6 +1282,7 @@ func buildExpensePartFromRows(
|
|||||||
rows []commonRepo.HppV2ExpenseCostRow,
|
rows []commonRepo.HppV2ExpenseCostRow,
|
||||||
code string,
|
code string,
|
||||||
title string,
|
title string,
|
||||||
|
scopes []string,
|
||||||
proration *HppV2Proration,
|
proration *HppV2Proration,
|
||||||
ratio float64,
|
ratio float64,
|
||||||
) *HppV2ComponentPart {
|
) *HppV2ComponentPart {
|
||||||
@@ -1005,6 +1313,7 @@ func buildExpensePartFromRows(
|
|||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: code,
|
Code: code,
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Scopes: append([]string{}, scopes...),
|
||||||
Total: total,
|
Total: total,
|
||||||
Proration: proration,
|
Proration: proration,
|
||||||
References: references,
|
References: references,
|
||||||
@@ -1015,6 +1324,7 @@ func buildChickinPartFromRows(
|
|||||||
rows []commonRepo.HppV2ChickinCostRow,
|
rows []commonRepo.HppV2ChickinCostRow,
|
||||||
code string,
|
code string,
|
||||||
title string,
|
title string,
|
||||||
|
scopes []string,
|
||||||
proration *HppV2Proration,
|
proration *HppV2Proration,
|
||||||
ratio float64,
|
ratio float64,
|
||||||
) *HppV2ComponentPart {
|
) *HppV2ComponentPart {
|
||||||
@@ -1048,6 +1358,7 @@ func buildChickinPartFromRows(
|
|||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: code,
|
Code: code,
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Scopes: append([]string{}, scopes...),
|
||||||
Total: total,
|
Total: total,
|
||||||
Proration: proration,
|
Proration: proration,
|
||||||
References: references,
|
References: references,
|
||||||
@@ -1065,3 +1376,48 @@ func componentHasScope(component *HppV2Component, scope string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func componentScopeTotal(component *HppV2Component, scope string) float64 {
|
||||||
|
if component == nil || scope == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
hasPartScopes := false
|
||||||
|
for _, part := range component.Parts {
|
||||||
|
if len(part.Scopes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasPartScopes = true
|
||||||
|
if partHasScope(&part, scope) {
|
||||||
|
total += part.Total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasPartScopes {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
if componentHasScope(component, scope) {
|
||||||
|
return component.Total
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func partHasScope(part *HppV2ComponentPart, scope string) bool {
|
||||||
|
if part == nil || scope == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, candidate := range part.Scopes {
|
||||||
|
if candidate == scope {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateOnly(value time.Time) time.Time {
|
||||||
|
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateOnly(value time.Time) string {
|
||||||
|
return dateOnly(value).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import (
|
|||||||
type hppV2RepoStub struct {
|
type hppV2RepoStub struct {
|
||||||
contextByPFK map[uint]*commonRepo.HppV2ProjectFlockKandangContext
|
contextByPFK map[uint]*commonRepo.HppV2ProjectFlockKandangContext
|
||||||
pfkIDsByProject map[uint][]uint
|
pfkIDsByProject map[uint][]uint
|
||||||
|
latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow
|
||||||
|
manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow
|
||||||
|
chickInDateByProject map[uint]*time.Time
|
||||||
|
depreciationByHouse map[string]map[int]float64
|
||||||
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
|
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
||||||
@@ -47,6 +51,35 @@ func (s *hppV2RepoStub) GetProjectFlockKandangIDs(_ context.Context, projectFloc
|
|||||||
return append([]uint{}, s.pfkIDsByProject[projectFlockId]...), nil
|
return append([]uint{}, s.pfkIDsByProject[projectFlockId]...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetLatestTransferInputByProjectFlockKandangID(_ context.Context, projectFlockKandangId uint, _ time.Time) (*commonRepo.HppV2LatestTransferInputRow, error) {
|
||||||
|
return s.latestTransferByPFK[projectFlockKandangId], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Context, projectFlockID uint) (*commonRepo.HppV2ManualDepreciationInputRow, error) {
|
||||||
|
return s.manualInputByProject[projectFlockID], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) {
|
||||||
|
return s.chickInDateByProject[projectFlockID], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetDepreciationPercents(_ context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) {
|
||||||
|
result := make(map[string]map[int]float64)
|
||||||
|
for _, houseType := range houseTypes {
|
||||||
|
source := s.depreciationByHouse[houseType]
|
||||||
|
if len(source) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[houseType] = make(map[int]float64)
|
||||||
|
for day, pct := range source {
|
||||||
|
if day <= maxDay {
|
||||||
|
result[houseType][day] = pct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
|
func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
|
||||||
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||||
}
|
}
|
||||||
@@ -161,8 +194,11 @@ func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
|
|||||||
if result == nil {
|
if result == nil {
|
||||||
t.Fatal("expected breakdown result")
|
t.Fatal("expected breakdown result")
|
||||||
}
|
}
|
||||||
if got := result.TotalProductionCost; got != 2950 {
|
if got := result.TotalPulletCost; got != 1150 {
|
||||||
t.Fatalf("expected total production cost 2950, got %v", got)
|
t.Fatalf("expected total pullet cost 1150, got %v", got)
|
||||||
|
}
|
||||||
|
if got := result.TotalProductionCost; got != 1800 {
|
||||||
|
t.Fatalf("expected total production cost 1800, got %v", got)
|
||||||
}
|
}
|
||||||
if len(result.Components) != 1 {
|
if len(result.Components) != 1 {
|
||||||
t.Fatalf("expected 1 component, got %d", len(result.Components))
|
t.Fatalf("expected 1 component, got %d", len(result.Components))
|
||||||
@@ -190,11 +226,11 @@ func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
|
|||||||
if component.Parts[0].Proration == nil || component.Parts[0].Proration.Ratio != 0.25 {
|
if component.Parts[0].Proration == nil || component.Parts[0].Proration.Ratio != 0.25 {
|
||||||
t.Fatalf("expected growing proration ratio 0.25, got %+v", component.Parts[0].Proration)
|
t.Fatalf("expected growing proration ratio 0.25, got %+v", component.Parts[0].Proration)
|
||||||
}
|
}
|
||||||
if result.Hpp.Estimation.HargaKg != 295 {
|
if result.Hpp.Estimation.HargaKg != 180 {
|
||||||
t.Fatalf("expected estimation harga/kg 295, got %v", result.Hpp.Estimation.HargaKg)
|
t.Fatalf("expected estimation harga/kg 180, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
}
|
}
|
||||||
if result.Hpp.Real.HargaKg != 737.5 {
|
if result.Hpp.Real.HargaKg != 450 {
|
||||||
t.Fatalf("expected real harga/kg 737.5, got %v", result.Hpp.Real.HargaKg)
|
t.Fatalf("expected real harga/kg 450, got %v", result.Hpp.Real.HargaKg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +372,14 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
|
|||||||
if componentTotals[hppV2ComponentOvk] != 450 {
|
if componentTotals[hppV2ComponentOvk] != 450 {
|
||||||
t.Fatalf("expected ovk total 450, got %v", componentTotals[hppV2ComponentOvk])
|
t.Fatalf("expected ovk total 450, got %v", componentTotals[hppV2ComponentOvk])
|
||||||
}
|
}
|
||||||
if result.TotalProductionCost != 950 {
|
if result.TotalPulletCost != 250 {
|
||||||
t.Fatalf("expected total production cost 950, got %v", result.TotalProductionCost)
|
t.Fatalf("expected total pullet cost 250, got %v", result.TotalPulletCost)
|
||||||
}
|
}
|
||||||
if result.Hpp.Estimation.HargaKg != 79.17 {
|
if result.TotalProductionCost != 700 {
|
||||||
t.Fatalf("expected estimation harga/kg 79.17, got %v", result.Hpp.Estimation.HargaKg)
|
t.Fatalf("expected total production cost 700, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 58.33 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 58.33, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,11 +542,204 @@ func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T)
|
|||||||
if componentTotals[hppV2ComponentBopEksp] != 88 {
|
if componentTotals[hppV2ComponentBopEksp] != 88 {
|
||||||
t.Fatalf("expected expedition BOP total 88, got %v", componentTotals[hppV2ComponentBopEksp])
|
t.Fatalf("expected expedition BOP total 88, got %v", componentTotals[hppV2ComponentBopEksp])
|
||||||
}
|
}
|
||||||
if result.TotalProductionCost != 358 {
|
if result.TotalPulletCost != 190 {
|
||||||
t.Fatalf("expected total production cost 358, got %v", result.TotalProductionCost)
|
t.Fatalf("expected total pullet cost 190, got %v", result.TotalPulletCost)
|
||||||
}
|
}
|
||||||
if result.Hpp.Estimation.HargaKg != 119.33 {
|
if result.TotalProductionCost != 168 {
|
||||||
t.Fatalf("expected estimation harga/kg 119.33, got %v", result.Hpp.Estimation.HargaKg)
|
t.Fatalf("expected total production cost 168, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 56 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 56, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_AddsDepreciationForNormalTransfer(t *testing.T) {
|
||||||
|
sourceChickIn := mustTime(t, "2026-01-01")
|
||||||
|
reportDate := sourceChickIn.AddDate(0, 0, 154)
|
||||||
|
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
50: {
|
||||||
|
ProjectFlockKandangID: 50,
|
||||||
|
ProjectFlockID: 10,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 500,
|
||||||
|
KandangName: "Kandang F",
|
||||||
|
LocationID: 21,
|
||||||
|
HouseType: "close_house",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
11: {501},
|
||||||
|
},
|
||||||
|
latestTransferByPFK: map[uint]*commonRepo.HppV2LatestTransferInputRow{
|
||||||
|
50: {
|
||||||
|
ProjectFlockKandangID: 50,
|
||||||
|
SourceProjectFlockID: 11,
|
||||||
|
TransferDate: mustTime(t, "2026-05-20"),
|
||||||
|
TransferQty: 100,
|
||||||
|
TransferID: 701,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chickInDateByProject: map[uint]*time.Time{
|
||||||
|
11: &sourceChickIn,
|
||||||
|
},
|
||||||
|
depreciationByHouse: map[string]map[int]float64{
|
||||||
|
"close_house": {
|
||||||
|
1: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||||
|
stubKey([]uint{501}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9301, SourceProductID: 41, SourceProductName: "Pakan Growing", Qty: 25, UnitPrice: 40, TotalCost: 1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{501}, nil): 100,
|
||||||
|
},
|
||||||
|
transferSummaryByPFK: map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}{
|
||||||
|
50: {projectFlockID: 11, totalQty: 100},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
50: {pieces: 20, kg: 10},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(50, &reportDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.TotalPulletCost != 1000 {
|
||||||
|
t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 100 {
|
||||||
|
t.Fatalf("expected total production cost 100, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
var depreciation *HppV2Component
|
||||||
|
for i := range result.Components {
|
||||||
|
if result.Components[i].Code == hppV2ComponentDepreciation {
|
||||||
|
depreciation = &result.Components[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depreciation == nil {
|
||||||
|
t.Fatal("expected depreciation component")
|
||||||
|
}
|
||||||
|
if depreciation.Total != 100 {
|
||||||
|
t.Fatalf("expected depreciation total 100, got %v", depreciation.Total)
|
||||||
|
}
|
||||||
|
if len(depreciation.Parts) != 1 {
|
||||||
|
t.Fatalf("expected single depreciation part, got %d", len(depreciation.Parts))
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["schedule_day"] != 1 {
|
||||||
|
t.Fatalf("expected schedule day 1, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["origin_date"] != "2026-01-01" {
|
||||||
|
t.Fatalf("expected origin date 2026-01-01, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 10 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 10, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_AddsDepreciationForManualCutoverFromCutoverDate(t *testing.T) {
|
||||||
|
originDate := mustTime(t, "2026-01-01")
|
||||||
|
cutoverDate := originDate.AddDate(0, 0, 155)
|
||||||
|
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
60: {
|
||||||
|
ProjectFlockKandangID: 60,
|
||||||
|
ProjectFlockID: 12,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 600,
|
||||||
|
KandangName: "Kandang G",
|
||||||
|
LocationID: 22,
|
||||||
|
HouseType: "close_house",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
12: {60},
|
||||||
|
},
|
||||||
|
manualInputByProject: map[uint]*commonRepo.HppV2ManualDepreciationInputRow{
|
||||||
|
12: {
|
||||||
|
ID: 801,
|
||||||
|
ProjectFlockID: 12,
|
||||||
|
TotalCost: 1000,
|
||||||
|
CutoverDate: cutoverDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chickInDateByProject: map[uint]*time.Time{
|
||||||
|
12: &originDate,
|
||||||
|
},
|
||||||
|
depreciationByHouse: map[string]map[int]float64{
|
||||||
|
"close_house": {
|
||||||
|
1: 10,
|
||||||
|
2: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{60}, nil): 100,
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
60: {pieces: 20, kg: 10},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(60, &cutoverDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.TotalPulletCost != 1000 {
|
||||||
|
t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 200 {
|
||||||
|
t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTotals := map[string]float64{}
|
||||||
|
for _, component := range result.Components {
|
||||||
|
componentTotals[component.Code] = component.Total
|
||||||
|
}
|
||||||
|
if componentTotals[hppV2ComponentManualPulletCost] != 1000 {
|
||||||
|
t.Fatalf("expected manual pullet cost 1000, got %v", componentTotals[hppV2ComponentManualPulletCost])
|
||||||
|
}
|
||||||
|
if componentTotals[hppV2ComponentDepreciation] != 200 {
|
||||||
|
t.Fatalf("expected depreciation 200, got %v", componentTotals[hppV2ComponentDepreciation])
|
||||||
|
}
|
||||||
|
|
||||||
|
var depreciation *HppV2Component
|
||||||
|
for i := range result.Components {
|
||||||
|
if result.Components[i].Code == hppV2ComponentDepreciation {
|
||||||
|
depreciation = &result.Components[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depreciation == nil || len(depreciation.Parts) != 1 {
|
||||||
|
t.Fatalf("expected one depreciation part, got %+v", depreciation)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["schedule_day"] != 2 {
|
||||||
|
t.Fatalf("expected schedule day 2, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if depreciation.Parts[0].Details["start_schedule_day"] != 2 {
|
||||||
|
t.Fatalf("expected start schedule day 2, got %+v", depreciation.Parts[0].Details)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 20 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 20, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_farm_depreciation_manual_inputs_cutover_date;
|
||||||
|
|
||||||
|
ALTER TABLE farm_depreciation_manual_inputs
|
||||||
|
DROP COLUMN IF EXISTS cutover_date;
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
ALTER TABLE farm_depreciation_manual_inputs
|
||||||
|
ADD COLUMN IF NOT EXISTS cutover_date DATE;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET cutover_date = COALESCE(cutover_date, DATE(created_at))
|
||||||
|
WHERE cutover_date IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE farm_depreciation_manual_inputs
|
||||||
|
ALTER COLUMN cutover_date SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_farm_depreciation_manual_inputs_cutover_date
|
||||||
|
ON farm_depreciation_manual_inputs (cutover_date);
|
||||||
@@ -6,6 +6,7 @@ type FarmDepreciationManualInput struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_manual_inputs_unique"`
|
ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_manual_inputs_unique"`
|
||||||
TotalCost float64 `gorm:"type:numeric(18,3);not null;default:0"`
|
TotalCost float64 `gorm:"type:numeric(18,3);not null;default:0"`
|
||||||
|
CutoverDate time.Time `gorm:"type:date;not null"`
|
||||||
Note *string `gorm:"type:text"`
|
Note *string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type ExpenseDepreciationManualInputRowDTO struct {
|
|||||||
ProjectFlockID int64 `json:"project_flock_id"`
|
ProjectFlockID int64 `json:"project_flock_id"`
|
||||||
FarmName string `json:"farm_name"`
|
FarmName string `json:"farm_name"`
|
||||||
TotalCost float64 `json:"total_cost"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
CutoverDate string `json:"cutover_date"`
|
||||||
Note *string `json:"note"`
|
Note *string `json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type FarmDepreciationManualInputRow struct {
|
|||||||
ProjectFlockID uint
|
ProjectFlockID uint
|
||||||
FarmName string
|
FarmName string
|
||||||
TotalCost float64
|
TotalCost float64
|
||||||
|
CutoverDate time.Time
|
||||||
Note *string
|
Note *string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +272,7 @@ func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms(
|
|||||||
fdmi.project_flock_id AS project_flock_id,
|
fdmi.project_flock_id AS project_flock_id,
|
||||||
pf.flock_name AS farm_name,
|
pf.flock_name AS farm_name,
|
||||||
fdmi.total_cost AS total_cost,
|
fdmi.total_cost AS total_cost,
|
||||||
|
fdmi.cutover_date AS cutover_date,
|
||||||
fdmi.note AS note
|
fdmi.note AS note
|
||||||
`).
|
`).
|
||||||
Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id").
|
Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id").
|
||||||
@@ -308,9 +310,10 @@ func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, r
|
|||||||
{Name: "project_flock_id"},
|
{Name: "project_flock_id"},
|
||||||
},
|
},
|
||||||
DoUpdates: clause.Assignments(map[string]any{
|
DoUpdates: clause.Assignments(map[string]any{
|
||||||
"total_cost": row.TotalCost,
|
"total_cost": row.TotalCost,
|
||||||
"note": row.Note,
|
"cutover_date": row.CutoverDate,
|
||||||
"updated_at": now,
|
"note": row.Note,
|
||||||
|
"updated_at": now,
|
||||||
}),
|
}),
|
||||||
}).
|
}).
|
||||||
Create(row).Error
|
Create(row).Error
|
||||||
@@ -320,7 +323,7 @@ func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, r
|
|||||||
|
|
||||||
return r.db.WithContext(ctx).
|
return r.db.WithContext(ctx).
|
||||||
Table("farm_depreciation_manual_inputs").
|
Table("farm_depreciation_manual_inputs").
|
||||||
Select("id, project_flock_id, total_cost, note").
|
Select("id, project_flock_id, total_cost, cutover_date, note").
|
||||||
Where("project_flock_id = ?", row.ProjectFlockId).
|
Where("project_flock_id = ?", row.ProjectFlockId).
|
||||||
Take(row).Error
|
Take(row).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]d
|
|||||||
ProjectFlockID: int64(row.ProjectFlockID),
|
ProjectFlockID: int64(row.ProjectFlockID),
|
||||||
FarmName: row.FarmName,
|
FarmName: row.FarmName,
|
||||||
TotalCost: row.TotalCost,
|
TotalCost: row.TotalCost,
|
||||||
|
CutoverDate: row.CutoverDate.Format("2006-01-02"),
|
||||||
Note: row.Note,
|
Note: row.Note,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -397,10 +398,19 @@ func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, re
|
|||||||
if s.ExpenseDepreciationRepo == nil {
|
if s.ExpenseDepreciationRepo == nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
|
||||||
}
|
}
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||||
|
}
|
||||||
|
cutoverDate, err := time.ParseInLocation("2006-01-02", req.CutoverDate, location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "cutover_date must follow format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
row := entity.FarmDepreciationManualInput{
|
row := entity.FarmDepreciationManualInput{
|
||||||
ProjectFlockId: req.ProjectFlockID,
|
ProjectFlockId: req.ProjectFlockID,
|
||||||
TotalCost: req.TotalCost,
|
TotalCost: req.TotalCost,
|
||||||
|
CutoverDate: cutoverDate,
|
||||||
Note: req.Note,
|
Note: req.Note,
|
||||||
}
|
}
|
||||||
if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil {
|
if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil {
|
||||||
@@ -411,6 +421,7 @@ func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, re
|
|||||||
ID: int64(row.Id),
|
ID: int64(row.Id),
|
||||||
ProjectFlockID: int64(row.ProjectFlockId),
|
ProjectFlockID: int64(row.ProjectFlockId),
|
||||||
TotalCost: row.TotalCost,
|
TotalCost: row.TotalCost,
|
||||||
|
CutoverDate: row.CutoverDate.Format("2006-01-02"),
|
||||||
Note: row.Note,
|
Note: row.Note,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ type ExpenseDepreciationQuery struct {
|
|||||||
type ExpenseDepreciationManualInputUpsert struct {
|
type ExpenseDepreciationManualInputUpsert struct {
|
||||||
ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"`
|
ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"`
|
||||||
TotalCost float64 `json:"total_cost" validate:"required,gte=0"`
|
TotalCost float64 `json:"total_cost" validate:"required,gte=0"`
|
||||||
|
CutoverDate string `json:"cutover_date" validate:"required,datetime=2006-01-02"`
|
||||||
Note *string `json:"note" validate:"omitempty,max=1000"`
|
Note *string `json:"note" validate:"omitempty,max=1000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user