mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 085d2f9bfe |
@@ -96,7 +96,6 @@ type HppV2FarmDepreciationSnapshotRow struct {
|
|||||||
DepreciationPercentEffective float64
|
DepreciationPercentEffective float64
|
||||||
DepreciationValue float64
|
DepreciationValue float64
|
||||||
PulletCostDayNTotal float64
|
PulletCostDayNTotal float64
|
||||||
Components []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppV2CostRepository interface {
|
type HppV2CostRepository interface {
|
||||||
@@ -405,7 +404,7 @@ func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeri
|
|||||||
var row HppV2FarmDepreciationSnapshotRow
|
var row HppV2FarmDepreciationSnapshotRow
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Table("farm_depreciation_snapshots").
|
Table("farm_depreciation_snapshots").
|
||||||
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total, components").
|
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total").
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
Where("period_date = DATE(?)", periodDate).
|
Where("period_date = DATE(?)", periodDate).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
@@ -1473,18 +1472,6 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
|
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
details := map[string]any{
|
|
||||||
"basis_total": snapshot.DepreciationValue,
|
|
||||||
"pullet_cost_day_n": appliedPulletCostDayN,
|
|
||||||
"depreciation_percent": depreciationPercent,
|
|
||||||
"snapshot_id": snapshot.ID,
|
|
||||||
"snapshot_period_date": formatDateOnly(snapshot.PeriodDate),
|
|
||||||
"snapshot_project_flock": snapshot.ProjectFlockID,
|
|
||||||
}
|
|
||||||
for key, value := range farmDepreciationSnapshotMetadata(snapshot.Components, projectFlockKandangId) {
|
|
||||||
details[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HppV2ComponentPart{
|
return &HppV2ComponentPart{
|
||||||
Code: hppV2PartDepreciationFarmSnapshot,
|
Code: hppV2PartDepreciationFarmSnapshot,
|
||||||
Title: "Farm Snapshot",
|
Title: "Farm Snapshot",
|
||||||
@@ -1496,7 +1483,14 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
Denominator: denominator,
|
Denominator: denominator,
|
||||||
Ratio: ratio,
|
Ratio: ratio,
|
||||||
},
|
},
|
||||||
Details: details,
|
Details: map[string]any{
|
||||||
|
"basis_total": snapshot.DepreciationValue,
|
||||||
|
"pullet_cost_day_n": appliedPulletCostDayN,
|
||||||
|
"depreciation_percent": depreciationPercent,
|
||||||
|
"snapshot_id": snapshot.ID,
|
||||||
|
"snapshot_period_date": formatDateOnly(snapshot.PeriodDate),
|
||||||
|
"snapshot_project_flock": snapshot.ProjectFlockID,
|
||||||
|
},
|
||||||
References: []HppV2Reference{
|
References: []HppV2Reference{
|
||||||
{
|
{
|
||||||
Type: "farm_depreciation_snapshot",
|
Type: "farm_depreciation_snapshot",
|
||||||
@@ -1510,84 +1504,6 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type farmDepreciationSnapshotComponents struct {
|
|
||||||
Kandang []farmDepreciationSnapshotKandangComponent `json:"kandang"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type farmDepreciationSnapshotKandangComponent struct {
|
|
||||||
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
|
||||||
DayN int `json:"day_n"`
|
|
||||||
MultiplicationPercent float64 `json:"multiplication_percentage"`
|
|
||||||
ChickinDate string `json:"chickin_date"`
|
|
||||||
OriginDate string `json:"origin_date"`
|
|
||||||
StandardEffectiveDate string `json:"standard_effective_date"`
|
|
||||||
Population float64 `json:"population"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func farmDepreciationSnapshotMetadata(raw []byte, projectFlockKandangID uint) map[string]any {
|
|
||||||
result := make(map[string]any)
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
var components farmDepreciationSnapshotComponents
|
|
||||||
if err := json.Unmarshal(raw, &components); err != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallback *farmDepreciationSnapshotKandangComponent
|
|
||||||
for i := range components.Kandang {
|
|
||||||
component := &components.Kandang[i]
|
|
||||||
if !component.hasDepreciationMetadata() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if component.ProjectFlockKandangID == projectFlockKandangID {
|
|
||||||
return component.snapshotDetails()
|
|
||||||
}
|
|
||||||
if fallback == nil {
|
|
||||||
fallback = component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fallback != nil {
|
|
||||||
return fallback.snapshotDetails()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c farmDepreciationSnapshotKandangComponent) hasDepreciationMetadata() bool {
|
|
||||||
return c.DayN > 0 ||
|
|
||||||
c.MultiplicationPercent > 0 ||
|
|
||||||
c.ChickinDate != "" ||
|
|
||||||
c.OriginDate != "" ||
|
|
||||||
c.StandardEffectiveDate != "" ||
|
|
||||||
c.Population > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c farmDepreciationSnapshotKandangComponent) snapshotDetails() map[string]any {
|
|
||||||
chickinDate := c.ChickinDate
|
|
||||||
if chickinDate == "" {
|
|
||||||
chickinDate = c.OriginDate
|
|
||||||
}
|
|
||||||
|
|
||||||
details := map[string]any{
|
|
||||||
"schedule_day": c.DayN,
|
|
||||||
"multiplication_percentage": c.MultiplicationPercent,
|
|
||||||
}
|
|
||||||
if chickinDate != "" {
|
|
||||||
details["origin_date"] = chickinDate
|
|
||||||
details["chickin_date"] = chickinDate
|
|
||||||
}
|
|
||||||
if c.StandardEffectiveDate != "" {
|
|
||||||
details["standard_effective_date"] = c.StandardEffectiveDate
|
|
||||||
}
|
|
||||||
if c.Population > 0 {
|
|
||||||
details["kandang_population"] = c.Population
|
|
||||||
}
|
|
||||||
|
|
||||||
return details
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
||||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
||||||
|
|||||||
@@ -825,28 +825,6 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
|||||||
DepreciationPercentEffective: 10,
|
DepreciationPercentEffective: 10,
|
||||||
DepreciationValue: 1000,
|
DepreciationValue: 1000,
|
||||||
PulletCostDayNTotal: 10000,
|
PulletCostDayNTotal: 10000,
|
||||||
Components: []byte(`{
|
|
||||||
"kandang_count": 2,
|
|
||||||
"total_population": 1000,
|
|
||||||
"kandang": [
|
|
||||||
{
|
|
||||||
"project_flock_kandang_id": 71,
|
|
||||||
"day_n": 5,
|
|
||||||
"multiplication_percentage": 0.95,
|
|
||||||
"chickin_date": "2026-01-02",
|
|
||||||
"standard_effective_date": "2026-06-01",
|
|
||||||
"population": 800
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"project_flock_kandang_id": 70,
|
|
||||||
"day_n": 7,
|
|
||||||
"multiplication_percentage": 0.93,
|
|
||||||
"chickin_date": "2026-01-01",
|
|
||||||
"standard_effective_date": "2026-06-02",
|
|
||||||
"population": 200
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
eggProductionByPFK: map[uint]struct {
|
eggProductionByPFK: map[uint]struct {
|
||||||
@@ -895,21 +873,6 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
|||||||
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
|
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
|
||||||
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
|
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
|
||||||
}
|
}
|
||||||
if depreciation.Parts[0].Details["schedule_day"] != 7 {
|
|
||||||
t.Fatalf("expected snapshot schedule_day 7, got %+v", depreciation.Parts[0].Details)
|
|
||||||
}
|
|
||||||
if depreciation.Parts[0].Details["multiplication_percentage"] != 0.93 {
|
|
||||||
t.Fatalf("expected snapshot multiplication_percentage 0.93, got %+v", depreciation.Parts[0].Details)
|
|
||||||
}
|
|
||||||
if depreciation.Parts[0].Details["chickin_date"] != "2026-01-01" {
|
|
||||||
t.Fatalf("expected snapshot chickin_date 2026-01-01, got %+v", depreciation.Parts[0].Details)
|
|
||||||
}
|
|
||||||
if depreciation.Parts[0].Details["standard_effective_date"] != "2026-06-02" {
|
|
||||||
t.Fatalf("expected snapshot standard_effective_date 2026-06-02, got %+v", depreciation.Parts[0].Details)
|
|
||||||
}
|
|
||||||
if depreciation.Parts[0].Details["kandang_population"] != float64(200) {
|
|
||||||
t.Fatalf("expected snapshot kandang_population 200, got %+v", depreciation.Parts[0].Details)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stubKey(ids []uint, flags []string) string {
|
func stubKey(ids []uint, flags []string) string {
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- Reverse UPSERT: hapus baris PFK 47 & 48 yang kemungkinan baru diinsert oleh up migration ini.
|
||||||
|
-- Jika sebelumnya sudah ada (ON CONFLICT DO UPDATE), baris ini akan terhapus —
|
||||||
|
-- restore manual dari backup jika diperlukan.
|
||||||
|
DELETE FROM farm_depreciation_manual_inputs
|
||||||
|
WHERE project_flock_id IN (47, 48);
|
||||||
|
|
||||||
|
-- UPDATE rows untuk PFK 4–27 tidak bisa di-reverse secara presisi:
|
||||||
|
-- nilai total_cost sebelum migration ini tidak tersimpan di migration history
|
||||||
|
-- (data awal di-load via cmd/import-farm-depreciation-manual-inputs dari Excel).
|
||||||
|
-- PFK 10 dan 11 tidak berubah (nilai sama dengan state dari migration 20260529144559).
|
||||||
|
-- Jika perlu rollback penuh: restore dari database backup atau re-import Excel lama.
|
||||||
|
|
||||||
|
-- Recompute snapshots setelah rollback
|
||||||
|
TRUNCATE TABLE farm_depreciation_snapshots;
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 1900157533.55,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 10;
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 146658321.066,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 13;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 51824694.138,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 17;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 15491774.796,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 8;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Cutover 2026-02-28 (lanjutan)
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 575074391.36, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 4;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 578360642.51, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 5;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 880983605.92, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 6;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 391669576.153, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 9;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 2521797832.14, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 11;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 139227054.164, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 12;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 380083106.836, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 14;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 705136853.847, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 15;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 209816474.000, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 18;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 557606867.000, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 19;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 239330456.11, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 20;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 4724203916.72, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 26;
|
||||||
|
|
||||||
|
-- Cutover 2026-05-15
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 5449963647.43, cutover_date = DATE '2026-05-15', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 27;
|
||||||
|
|
||||||
|
-- Cutover 2026-06-08 (upsert — row mungkin belum ada)
|
||||||
|
INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at)
|
||||||
|
VALUES (47, 5395429899.42, DATE '2026-06-08', NOW(), NOW())
|
||||||
|
ON CONFLICT (project_flock_id) DO UPDATE
|
||||||
|
SET total_cost = EXCLUDED.total_cost,
|
||||||
|
cutover_date = EXCLUDED.cutover_date,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- Cutover 2026-06-16 (upsert — row mungkin belum ada)
|
||||||
|
INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at)
|
||||||
|
VALUES (48, 5514616442.08, DATE '2026-06-16', NOW(), NOW())
|
||||||
|
ON CONFLICT (project_flock_id) DO UPDATE
|
||||||
|
SET total_cost = EXCLUDED.total_cost,
|
||||||
|
cutover_date = EXCLUDED.cutover_date,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- Pengaman: pastikan snapshot di-recompute dengan total_cost baru
|
||||||
|
-- saat user request /api/reports/expense/depreciation
|
||||||
|
TRUNCATE TABLE farm_depreciation_snapshots;
|
||||||
@@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordingService interface {
|
type RecordingService interface {
|
||||||
@@ -586,10 +585,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
action := entity.ApprovalActionCreated
|
action := entity.ApprovalActionCreated
|
||||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
||||||
@@ -892,12 +887,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasStockChanges {
|
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action := entity.ApprovalActionUpdated
|
action := entity.ApprovalActionUpdated
|
||||||
actorID := recordingEntity.CreatedBy
|
actorID := recordingEntity.CreatedBy
|
||||||
@@ -1159,10 +1148,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1949,172 +1934,6 @@ func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx con
|
|||||||
return row.ChickInDate, nil
|
return row.ChickInDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) syncFarmDepreciationManualInputFromRecordingStocks(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
projectFlockKandangID uint,
|
|
||||||
fallbackCutoverDate time.Time,
|
|
||||||
) error {
|
|
||||||
if projectFlockKandangID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targetDB := s.Repository.DB()
|
|
||||||
if tx != nil {
|
|
||||||
targetDB = tx
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlockID, err := s.resolveProjectFlockIDByProjectFlockKandangID(ctx, targetDB, projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalCost, err := s.sumNoTransferRecordingStockCostByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := s.getFarmDepreciationManualInputByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cutoverDate := normalizeDateOnlyUTC(fallbackCutoverDate)
|
|
||||||
if existing != nil && !existing.CutoverDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(existing.CutoverDate)
|
|
||||||
}
|
|
||||||
if cutoverDate.IsZero() {
|
|
||||||
earliestDate, dateErr := s.getEarliestNoTransferRecordingDateByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if dateErr != nil {
|
|
||||||
return dateErr
|
|
||||||
}
|
|
||||||
if earliestDate != nil && !earliestDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(*earliestDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cutoverDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(time.Now().UTC())
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
|
||||||
row := entity.FarmDepreciationManualInput{
|
|
||||||
ProjectFlockId: projectFlockID,
|
|
||||||
TotalCost: totalCost,
|
|
||||||
CutoverDate: cutoverDate,
|
|
||||||
}
|
|
||||||
if existing != nil {
|
|
||||||
row.Note = existing.Note
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetDB.WithContext(ctx).
|
|
||||||
Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "project_flock_id"}},
|
|
||||||
DoUpdates: clause.Assignments(map[string]any{
|
|
||||||
"total_cost": row.TotalCost,
|
|
||||||
"cutover_date": row.CutoverDate,
|
|
||||||
"updated_at": now,
|
|
||||||
}),
|
|
||||||
}).
|
|
||||||
Create(&row).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) resolveProjectFlockIDByProjectFlockKandangID(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (uint, error) {
|
|
||||||
var row struct {
|
|
||||||
ProjectFlockID uint `gorm:"column:project_flock_id"`
|
|
||||||
}
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("project_flock_kandangs").
|
|
||||||
Select("project_flock_id").
|
|
||||||
Where("id = ?", projectFlockKandangID).
|
|
||||||
Take(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return row.ProjectFlockID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) sumNoTransferRecordingStockCostByProjectFlockID(ctx context.Context, db *gorm.DB, projectFlockID uint) (float64, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var total float64
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("recording_stocks AS rs").
|
|
||||||
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)").
|
|
||||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Joins(
|
|
||||||
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
||||||
fifo.UsableKeyRecordingStock.String(),
|
|
||||||
fifo.StockableKeyPurchaseItems.String(),
|
|
||||||
entity.StockAllocationStatusActive,
|
|
||||||
entity.StockAllocationPurposeConsume,
|
|
||||||
).
|
|
||||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("rs.project_flock_kandang_id IS NULL").
|
|
||||||
Scan(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getFarmDepreciationManualInputByProjectFlockID(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
projectFlockID uint,
|
|
||||||
) (*entity.FarmDepreciationManualInput, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var row entity.FarmDepreciationManualInput
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
|
||||||
Take(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getEarliestNoTransferRecordingDateByProjectFlockID(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
projectFlockID uint,
|
|
||||||
) (*time.Time, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var row struct {
|
|
||||||
RecordDate *time.Time `gorm:"column:record_date"`
|
|
||||||
}
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("recording_stocks AS rs").
|
|
||||||
Select("MIN(r.record_datetime) AS record_date").
|
|
||||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("rs.project_flock_kandang_id IS NULL").
|
|
||||||
Scan(&row).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return row.RecordDate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pfk *entity.ProjectFlockKandang,
|
pfk *entity.ProjectFlockKandang,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type ExpenseDepreciationV2MetaDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseDepreciationV2RowDTO struct {
|
type ExpenseDepreciationV2RowDTO struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
|
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
|
||||||
DepreciationValue float64 `json:"depreciation_value"`
|
DepreciationValue float64 `json:"depreciation_value"`
|
||||||
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
|
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
|
||||||
@@ -60,6 +60,7 @@ type ExpenseDepreciationV2RowDTO struct {
|
|||||||
TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"`
|
TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"`
|
||||||
StandardEffectiveDate string `json:"standard_effective_date,omitempty"`
|
StandardEffectiveDate string `json:"standard_effective_date,omitempty"`
|
||||||
TotalPopulation float64 `json:"total_population"`
|
TotalPopulation float64 `json:"total_population"`
|
||||||
|
Components any `json:"components"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
|
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExpenseDepreciationRowDTOComponentsJSONContract(t *testing.T) {
|
|
||||||
v1 := ExpenseDepreciationRowDTO{
|
|
||||||
ProjectFlockID: 1,
|
|
||||||
FarmName: "Farm A",
|
|
||||||
Period: "2026-06-05",
|
|
||||||
Components: map[string]any{"kandang_count": 1},
|
|
||||||
}
|
|
||||||
rawV1, err := json.Marshal(v1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal v1 dto: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var decodedV1 map[string]any
|
|
||||||
if err := json.Unmarshal(rawV1, &decodedV1); err != nil {
|
|
||||||
t.Fatalf("unmarshal v1 dto: %v", err)
|
|
||||||
}
|
|
||||||
if _, ok := decodedV1["components"]; !ok {
|
|
||||||
t.Fatalf("expected v1 components to be present, got %s", string(rawV1))
|
|
||||||
}
|
|
||||||
|
|
||||||
v2 := ExpenseDepreciationV2RowDTO{
|
|
||||||
Date: "2026-06-05",
|
|
||||||
DepreciationPercentEffective: 10,
|
|
||||||
DepreciationValue: 100,
|
|
||||||
PulletCostDayNTotal: 1000,
|
|
||||||
MultiplicationPercentage: 0.9,
|
|
||||||
DayN: 2,
|
|
||||||
ChickinDate: "2026-01-01",
|
|
||||||
TotalValuePulletAfterDepreciation: 900,
|
|
||||||
TotalPopulation: 100,
|
|
||||||
}
|
|
||||||
rawV2, err := json.Marshal(v2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal v2 dto: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var decodedV2 map[string]any
|
|
||||||
if err := json.Unmarshal(rawV2, &decodedV2); err != nil {
|
|
||||||
t.Fatalf("unmarshal v2 dto: %v", err)
|
|
||||||
}
|
|
||||||
if _, ok := decodedV2["components"]; ok {
|
|
||||||
t.Fatalf("expected v2 components to be omitted, got %s", string(rawV2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppByDeliver
|
|||||||
realizationDate = *mdp.DeliveryDate
|
realizationDate = *mdp.DeliveryDate
|
||||||
}
|
}
|
||||||
|
|
||||||
totalWeightKg := mdp.TotalWeight
|
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
||||||
salesAmount := totalWeightKg * mdp.UnitPrice
|
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||||
|
|
||||||
var hpp float64
|
var hpp float64
|
||||||
|
|||||||
@@ -90,12 +90,6 @@ func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) DeleteSnapshotsByFarmIDs(_ context.Context, farmIDs []uint) error {
|
|
||||||
m.deleteCalled = true
|
|
||||||
m.deleteFarmIDs = append([]uint{}, farmIDs...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
|
func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
|
||||||
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,10 +423,7 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
var totalDepreciationValue float64
|
var totalDepreciationValue float64
|
||||||
var totalPulletCostDayN float64
|
var totalPulletCostDayN float64
|
||||||
var totalPopulation float64
|
var totalPopulation float64
|
||||||
var multiplicationPercentage float64
|
var allKandangComponents []depreciationKandangComponent
|
||||||
var dayN int
|
|
||||||
var chickinDate string
|
|
||||||
var standardEffectiveDate string
|
|
||||||
|
|
||||||
for _, kandangID := range kandangIDs {
|
for _, kandangID := range kandangIDs {
|
||||||
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
|
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
|
||||||
@@ -447,33 +444,72 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
partPulletCostDayN := hppV2DetailFloat(part.Details, "pullet_cost_day_n")
|
houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType)
|
||||||
partPopulation := hppV2DetailFloat(part.Details, "kandang_population")
|
component := depreciationKandangComponent{
|
||||||
partDayN := hppV2DetailInt(part.Details, "schedule_day")
|
ProjectFlockKandangID: breakdown.ProjectFlockKandangID,
|
||||||
partMultiplicationPercentage := hppV2DetailFloat(part.Details, "multiplication_percentage")
|
KandangID: breakdown.KandangID,
|
||||||
partChickinDate := hppV2DetailString(part.Details, "chickin_date")
|
KandangName: breakdown.KandangName,
|
||||||
if partChickinDate == "" {
|
SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"),
|
||||||
partChickinDate = hppV2DetailString(part.Details, "origin_date")
|
HouseType: houseType,
|
||||||
|
DayN: hppV2DetailInt(part.Details, "schedule_day"),
|
||||||
|
DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"),
|
||||||
|
MultiplicationPercentage: hppV2DetailFloat(part.Details, "multiplication_percentage"),
|
||||||
|
PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"),
|
||||||
|
DepreciationValue: part.Total,
|
||||||
|
TotalValuePulletAfterDepreciation: hppV2DetailFloat(part.Details, "total_value_pullet_after_depreciation"),
|
||||||
|
DepreciationSource: part.Code,
|
||||||
|
OriginDate: hppV2DetailString(part.Details, "origin_date"),
|
||||||
|
ChickinDate: hppV2DetailString(part.Details, "origin_date"),
|
||||||
|
StandardEffectiveDate: hppV2DetailString(part.Details, "standard_effective_date"),
|
||||||
|
Population: hppV2DetailFloat(part.Details, "kandang_population"),
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPulletCostDayN += partPulletCostDayN
|
if component.HouseType == "" {
|
||||||
totalDepreciationValue += part.Total
|
component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type"))
|
||||||
totalPopulation += partPopulation
|
|
||||||
|
|
||||||
if dayN == 0 && multiplicationPercentage == 0 && chickinDate == "" &&
|
|
||||||
(partDayN > 0 || partMultiplicationPercentage > 0 || partChickinDate != "") {
|
|
||||||
dayN = partDayN
|
|
||||||
multiplicationPercentage = partMultiplicationPercentage
|
|
||||||
chickinDate = partChickinDate
|
|
||||||
standardEffectiveDate = hppV2DetailString(part.Details, "standard_effective_date")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil {
|
||||||
|
component.TransferID = ref.ID
|
||||||
|
component.TransferDate = ref.Date
|
||||||
|
component.TransferQty = ref.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.Code == "manual_cutover" {
|
||||||
|
if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 {
|
||||||
|
component.StartScheduleDay = &startDay
|
||||||
|
}
|
||||||
|
component.CutoverDate = hppV2DetailString(part.Details, "cutover_date")
|
||||||
|
if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 {
|
||||||
|
component.ManualInputID = &manualID
|
||||||
|
}
|
||||||
|
if component.ManualInputID == nil {
|
||||||
|
if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 {
|
||||||
|
manualID := ref.ID
|
||||||
|
component.ManualInputID = &manualID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPulletCostDayN += component.PulletCostDayN
|
||||||
|
totalDepreciationValue += component.DepreciationValue
|
||||||
|
totalPopulation += component.Population
|
||||||
|
allKandangComponents = append(allKandangComponents, component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
||||||
|
|
||||||
|
components := depreciationFarmComponents{
|
||||||
|
KandangCount: len(allKandangComponents),
|
||||||
|
TotalPopulation: totalPopulation,
|
||||||
|
Kandang: allKandangComponents,
|
||||||
|
}
|
||||||
|
componentsJSON, _ := json.Marshal(components)
|
||||||
|
|
||||||
|
multiplicationPercentage, dayN, chickinDate, standardEffectiveDate := depreciationSnapshotInfo(parseSnapshotComponents(componentsJSON))
|
||||||
|
|
||||||
rows = append(rows, dto.ExpenseDepreciationV2RowDTO{
|
rows = append(rows, dto.ExpenseDepreciationV2RowDTO{
|
||||||
Date: dayStr,
|
Date: dayStr,
|
||||||
DepreciationPercentEffective: effectivePercent,
|
DepreciationPercentEffective: effectivePercent,
|
||||||
DepreciationValue: totalDepreciationValue,
|
DepreciationValue: totalDepreciationValue,
|
||||||
PulletCostDayNTotal: totalPulletCostDayN,
|
PulletCostDayNTotal: totalPulletCostDayN,
|
||||||
@@ -483,6 +519,7 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
|||||||
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
|
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
|
||||||
StandardEffectiveDate: standardEffectiveDate,
|
StandardEffectiveDate: standardEffectiveDate,
|
||||||
TotalPopulation: totalPopulation,
|
TotalPopulation: totalPopulation,
|
||||||
|
Components: parseSnapshotComponents(componentsJSON),
|
||||||
})
|
})
|
||||||
actualDays++
|
actualDays++
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user