mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 14:55:42 +00:00
Merge branch 'codex/recording' into 'development'
Codex/recording See merge request mbugroup/lti-api!435
This commit is contained in:
@@ -151,7 +151,7 @@ func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKa
|
|||||||
).
|
).
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime <= ?", *date).
|
Where("r.record_datetime <= ?", *date).
|
||||||
Where("f.name = ?", utils.FlagPakan).
|
Where("f.name = ?", utils.FlagPakan).
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
@@ -202,7 +202,7 @@ func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKan
|
|||||||
).
|
).
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime <= ?", *date).
|
Where("r.record_datetime <= ?", *date).
|
||||||
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ type HppV2CostRepository interface {
|
|||||||
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)
|
GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error)
|
||||||
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error)
|
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error)
|
||||||
|
GetRecordingStockRoutingAdjustmentCostByProjectFlockID(ctx context.Context, projectFlockID uint, periodDate time.Time) (float64, error)
|
||||||
GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error)
|
GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error)
|
||||||
GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
|
GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
|
||||||
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
|
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
|
||||||
@@ -249,6 +250,82 @@ func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
|
|||||||
return &row, nil
|
return &row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlockID(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockID uint,
|
||||||
|
periodDate time.Time,
|
||||||
|
) (float64, error) {
|
||||||
|
if projectFlockID == 0 || periodDate.IsZero() {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := []utils.FlagType{
|
||||||
|
utils.FlagPakan,
|
||||||
|
utils.FlagOVK,
|
||||||
|
utils.FlagObat,
|
||||||
|
utils.FlagVitamin,
|
||||||
|
utils.FlagKimia,
|
||||||
|
}
|
||||||
|
transferExistsCondition := `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM laying_transfer_targets AS ltt
|
||||||
|
JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id
|
||||||
|
WHERE ltt.deleted_at IS NULL
|
||||||
|
AND lt.deleted_at IS NULL
|
||||||
|
AND lt.executed_at IS NOT NULL
|
||||||
|
AND ltt.target_project_flock_kandang_id = r.project_flock_kandangs_id
|
||||||
|
AND COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) <= DATE(?)
|
||||||
|
AND (
|
||||||
|
SELECT a.action
|
||||||
|
FROM approvals a
|
||||||
|
WHERE a.approvable_type = ?
|
||||||
|
AND a.approvable_id = lt.id
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
) = ?
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
err := r.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_rec ON pfk_rec.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins(
|
||||||
|
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? 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_rec.project_flock_id = ?", projectFlockID).
|
||||||
|
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
|
||||||
|
Where(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)",
|
||||||
|
transferExistsCondition,
|
||||||
|
transferExistsCondition,
|
||||||
|
),
|
||||||
|
periodDate,
|
||||||
|
string(utils.ApprovalWorkflowTransferToLaying),
|
||||||
|
entity.ApprovalActionApproved,
|
||||||
|
periodDate,
|
||||||
|
string(utils.ApprovalWorkflowTransferToLaying),
|
||||||
|
entity.ApprovalActionApproved,
|
||||||
|
).
|
||||||
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(
|
func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
projectFlockID uint,
|
projectFlockID uint,
|
||||||
@@ -393,7 +470,7 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
|||||||
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id").
|
Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id").
|
||||||
Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id").
|
Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id").
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime <= ?", *date).
|
Where("r.record_datetime <= ?", *date).
|
||||||
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames).
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames).
|
||||||
Group(`
|
Group(`
|
||||||
@@ -755,7 +832,7 @@ func (r *HppV2RepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlock
|
|||||||
).
|
).
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime <= ?", *date).
|
Where("r.record_datetime <= ?", *date).
|
||||||
Where("f.name = ?", utils.FlagPakan).
|
Where("f.name = ?", utils.FlagPakan).
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -96,6 +97,116 @@ func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments(
|
|||||||
assertFloatEquals(t, totalWeightKg, 1.4)
|
assertFloatEquals(t, totalWeightKg, 1.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) {
|
||||||
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
approvalType := utils.ApprovalWorkflowTransferToLaying.String()
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES
|
||||||
|
(101, 1, 1),
|
||||||
|
(102, 2, 1),
|
||||||
|
(103, 3, 1),
|
||||||
|
(104, 4, 1),
|
||||||
|
(105, 5, 1),
|
||||||
|
(201, 6, 2)`,
|
||||||
|
`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES
|
||||||
|
(1, 101, '2026-04-10 08:00:00', NULL),
|
||||||
|
(2, 101, '2026-04-10 08:05:00', NULL),
|
||||||
|
(3, 101, '2026-04-10 08:10:00', NULL),
|
||||||
|
(4, 102, '2026-04-10 08:15:00', NULL),
|
||||||
|
(5, 102, '2026-04-10 08:20:00', NULL),
|
||||||
|
(6, 103, '2026-04-12 08:00:00', NULL),
|
||||||
|
(7, 103, '2026-04-12 08:05:00', NULL),
|
||||||
|
(8, 104, '2026-04-12 08:10:00', NULL),
|
||||||
|
(9, 104, '2026-04-12 08:15:00', NULL),
|
||||||
|
(10, 105, '2026-04-12 08:20:00', NULL),
|
||||||
|
(11, 105, '2026-04-12 08:25:00', NULL)`,
|
||||||
|
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES
|
||||||
|
(501, 201, 10, NULL),
|
||||||
|
(502, 201, 10, NULL),
|
||||||
|
(503, 201, 10, NULL),
|
||||||
|
(504, 201, 10, NULL),
|
||||||
|
(505, 201, 10, NULL),
|
||||||
|
(506, 201, 10, NULL),
|
||||||
|
(507, 201, 10, NULL),
|
||||||
|
(508, 201, 10, NULL),
|
||||||
|
(509, 201, 10, NULL),
|
||||||
|
(510, 201, 10, NULL),
|
||||||
|
(511, 201, 10, NULL)`,
|
||||||
|
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES
|
||||||
|
(10, 'products', 10, 'PAKAN')`,
|
||||||
|
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, project_flock_kandang_id) VALUES
|
||||||
|
(101, 1, 501, NULL),
|
||||||
|
(102, 2, 502, 201),
|
||||||
|
(103, 3, 503, 101),
|
||||||
|
(104, 4, 504, NULL),
|
||||||
|
(105, 5, 505, 201),
|
||||||
|
(106, 6, 506, NULL),
|
||||||
|
(107, 7, 507, 201),
|
||||||
|
(108, 8, 508, NULL),
|
||||||
|
(109, 9, 509, 201),
|
||||||
|
(110, 10, 510, NULL),
|
||||||
|
(111, 11, 511, 201)`,
|
||||||
|
`INSERT INTO purchase_items (id, product_id, price) VALUES
|
||||||
|
(601, 10, 100),
|
||||||
|
(602, 10, 110),
|
||||||
|
(603, 10, 120),
|
||||||
|
(604, 10, 130),
|
||||||
|
(605, 10, 140),
|
||||||
|
(606, 10, 150),
|
||||||
|
(607, 10, 160),
|
||||||
|
(608, 10, 170),
|
||||||
|
(609, 10, 180),
|
||||||
|
(610, 10, 190),
|
||||||
|
(611, 10, 200)`,
|
||||||
|
`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose, qty) VALUES
|
||||||
|
(9001, 'RECORDING_STOCK', 101, 'PURCHASE_ITEMS', 601, 'ACTIVE', 'CONSUME', 2),
|
||||||
|
(9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9004, 'RECORDING_STOCK', 104, 'PURCHASE_ITEMS', 604, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9005, 'RECORDING_STOCK', 105, 'PURCHASE_ITEMS', 605, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9006, 'RECORDING_STOCK', 106, 'PURCHASE_ITEMS', 606, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9007, 'RECORDING_STOCK', 107, 'PURCHASE_ITEMS', 607, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9008, 'RECORDING_STOCK', 108, 'PURCHASE_ITEMS', 608, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9009, 'RECORDING_STOCK', 109, 'PURCHASE_ITEMS', 609, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9010, 'RECORDING_STOCK', 110, 'PURCHASE_ITEMS', 610, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9011, 'RECORDING_STOCK', 111, 'PURCHASE_ITEMS', 611, 'ACTIVE', 'CONSUME', 1)`,
|
||||||
|
`INSERT INTO laying_transfers (id, transfer_date, effective_move_date, economic_cutoff_date, executed_at, deleted_at) VALUES
|
||||||
|
(1001, '2026-04-04', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
|
||||||
|
(1002, '2026-05-01', '2026-05-01', NULL, '2026-05-01 00:00:00', NULL),
|
||||||
|
(1003, '2026-04-03', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
|
||||||
|
(1004, '2026-04-03', '2026-04-05', NULL, NULL, NULL)`,
|
||||||
|
`INSERT INTO laying_transfer_targets (id, laying_transfer_id, target_project_flock_kandang_id, deleted_at) VALUES
|
||||||
|
(2001, 1001, 101, NULL),
|
||||||
|
(2002, 1002, 103, NULL),
|
||||||
|
(2003, 1003, 104, NULL),
|
||||||
|
(2004, 1004, 105, NULL)`,
|
||||||
|
fmt.Sprintf(`INSERT INTO approvals (id, approvable_type, approvable_id, action) VALUES
|
||||||
|
(3001, '%s', 1001, 'APPROVED'),
|
||||||
|
(3002, '%s', 1002, 'APPROVED'),
|
||||||
|
(3003, '%s', 1003, 'APPROVED'),
|
||||||
|
(3004, '%s', 1003, 'REJECTED'),
|
||||||
|
(3005, '%s', 1004, 'APPROVED')`,
|
||||||
|
approvalType, approvalType, approvalType, approvalType, approvalType),
|
||||||
|
)
|
||||||
|
|
||||||
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
|
|
||||||
|
periodDate := mustJakartaTime(t, "2026-04-30 00:00:00")
|
||||||
|
total, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, periodDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
assertFloatEquals(t, total, 750)
|
||||||
|
|
||||||
|
earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
|
||||||
|
earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
assertFloatEquals(t, earlyTotal, 240)
|
||||||
|
}
|
||||||
|
|
||||||
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -111,6 +222,12 @@ func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
|||||||
record_datetime DATETIME NULL,
|
record_datetime DATETIME NULL,
|
||||||
deleted_at DATETIME NULL
|
deleted_at DATETIME NULL
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE recording_stocks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
recording_id INTEGER NULL,
|
||||||
|
product_warehouse_id INTEGER NULL,
|
||||||
|
project_flock_kandang_id INTEGER NULL
|
||||||
|
)`,
|
||||||
`CREATE TABLE recording_eggs (
|
`CREATE TABLE recording_eggs (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
recording_id INTEGER NULL,
|
recording_id INTEGER NULL,
|
||||||
@@ -174,6 +291,11 @@ func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
|||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
product_warehouse_id INTEGER NULL
|
product_warehouse_id INTEGER NULL
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE purchase_items (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
product_id INTEGER NULL,
|
||||||
|
price NUMERIC(15,3) NULL
|
||||||
|
)`,
|
||||||
`CREATE TABLE marketing_delivery_products (
|
`CREATE TABLE marketing_delivery_products (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
marketing_product_id INTEGER NULL,
|
marketing_product_id INTEGER NULL,
|
||||||
@@ -187,6 +309,26 @@ func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
|||||||
flagable_id INTEGER NULL,
|
flagable_id INTEGER NULL,
|
||||||
name TEXT NULL
|
name TEXT NULL
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE laying_transfers (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
transfer_date DATETIME NULL,
|
||||||
|
effective_move_date DATETIME NULL,
|
||||||
|
economic_cutoff_date DATETIME NULL,
|
||||||
|
executed_at DATETIME NULL,
|
||||||
|
deleted_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE laying_transfer_targets (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
laying_transfer_id INTEGER NULL,
|
||||||
|
target_project_flock_kandang_id INTEGER NULL,
|
||||||
|
deleted_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE approvals (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
approvable_type TEXT NULL,
|
||||||
|
approvable_id INTEGER NULL,
|
||||||
|
action TEXT NULL
|
||||||
|
)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const (
|
|||||||
hppV2ComponentBopRegular = "BOP_REGULAR"
|
hppV2ComponentBopRegular = "BOP_REGULAR"
|
||||||
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
||||||
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
|
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
|
||||||
|
hppV2ComponentRecordingStockRoute = "RECORDING_STOCK_ROUTE"
|
||||||
hppV2ComponentDepreciation = "DEPRECIATION"
|
hppV2ComponentDepreciation = "DEPRECIATION"
|
||||||
hppV2PartGrowingNormal = "growing_normal"
|
hppV2PartGrowingNormal = "growing_normal"
|
||||||
hppV2PartGrowingCutover = "growing_cutover"
|
hppV2PartGrowingCutover = "growing_cutover"
|
||||||
@@ -26,6 +27,7 @@ const (
|
|||||||
hppV2PartLayingDirect = "laying_direct"
|
hppV2PartLayingDirect = "laying_direct"
|
||||||
hppV2PartLayingFarm = "laying_farm"
|
hppV2PartLayingFarm = "laying_farm"
|
||||||
hppV2PartManualCutover = "manual_cutover"
|
hppV2PartManualCutover = "manual_cutover"
|
||||||
|
hppV2PartRecordingStockRoute = "recording_stock_route"
|
||||||
hppV2PartDepreciationNormal = "normal_transfer"
|
hppV2PartDepreciationNormal = "normal_transfer"
|
||||||
hppV2PartDepreciationCutover = "manual_cutover"
|
hppV2PartDepreciationCutover = "manual_cutover"
|
||||||
hppV2PartDepreciationFarmSnapshot = "farm_snapshot"
|
hppV2PartDepreciationFarmSnapshot = "farm_snapshot"
|
||||||
@@ -190,6 +192,12 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
|||||||
}
|
}
|
||||||
appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent)
|
appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent)
|
||||||
|
|
||||||
|
recordingStockRouteComponent, err := s.getRecordingStockRouteComponent(projectFlockKandangId, contextRow, startOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appendComponent(hppV2ComponentRecordingStockRoute, recordingStockRouteComponent)
|
||||||
|
|
||||||
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost)
|
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1064,6 +1072,100 @@ func (s *hppV2Service) getManualPulletCostComponent(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) getRecordingStockRouteComponent(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
periodDate time.Time,
|
||||||
|
) (*HppV2Component, error) {
|
||||||
|
if s.hppRepo == nil || contextRow == nil || periodDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
farmTotalCost, err := s.hppRepo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(
|
||||||
|
context.Background(),
|
||||||
|
contextRow.ProjectFlockID,
|
||||||
|
periodDate,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if farmTotalCost <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(farmPFKIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if totalPopulation <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if targetPopulation <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := targetPopulation / totalPopulation
|
||||||
|
if ratio <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appliedTotal := farmTotalCost * ratio
|
||||||
|
if appliedTotal <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
part := HppV2ComponentPart{
|
||||||
|
Code: hppV2PartRecordingStockRoute,
|
||||||
|
Title: "Recording Stock Route",
|
||||||
|
Scopes: []string{hppV2ScopePulletCost},
|
||||||
|
Total: appliedTotal,
|
||||||
|
Proration: &HppV2Proration{
|
||||||
|
Basis: hppV2ProrationPopulation,
|
||||||
|
Numerator: targetPopulation,
|
||||||
|
Denominator: totalPopulation,
|
||||||
|
Ratio: ratio,
|
||||||
|
},
|
||||||
|
Details: map[string]any{
|
||||||
|
"period_date": formatDateOnly(periodDate),
|
||||||
|
"farm_total_cost": farmTotalCost,
|
||||||
|
"target_population": targetPopulation,
|
||||||
|
"farm_population": totalPopulation,
|
||||||
|
"project_flock_id": contextRow.ProjectFlockID,
|
||||||
|
"project_flock_kandang_id": projectFlockKandangId,
|
||||||
|
},
|
||||||
|
References: []HppV2Reference{
|
||||||
|
{
|
||||||
|
Type: "recording_stock_route",
|
||||||
|
Date: formatDateOnly(periodDate),
|
||||||
|
Qty: 1,
|
||||||
|
Total: farmTotalCost,
|
||||||
|
AppliedTotal: appliedTotal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: hppV2ComponentRecordingStockRoute,
|
||||||
|
Title: "Recording Stock Route",
|
||||||
|
Scopes: []string{hppV2ScopePulletCost},
|
||||||
|
Total: appliedTotal,
|
||||||
|
Parts: []HppV2ComponentPart{part},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) getDepreciationComponent(
|
func (s *hppV2Service) getDepreciationComponent(
|
||||||
projectFlockKandangId uint,
|
projectFlockKandangId uint,
|
||||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type hppV2RepoStub struct {
|
|||||||
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
||||||
expenseRowsByPFKKey map[string][]commonRepo.HppV2ExpenseCostRow
|
expenseRowsByPFKKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
|
expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
|
routeCostByProject map[uint]float64
|
||||||
totalPopulationByKey map[string]float64
|
totalPopulationByKey map[string]float64
|
||||||
transferSummaryByPFK map[uint]struct {
|
transferSummaryByPFK map[uint]struct {
|
||||||
projectFlockID uint
|
projectFlockID uint
|
||||||
@@ -60,6 +61,10 @@ func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Con
|
|||||||
return s.manualInputByProject[projectFlockID], nil
|
return s.manualInputByProject[projectFlockID], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetRecordingStockRoutingAdjustmentCostByProjectFlockID(_ context.Context, projectFlockID uint, _ time.Time) (float64, error) {
|
||||||
|
return s.routeCostByProject[projectFlockID], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *hppV2RepoStub) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(_ context.Context, projectFlockID uint, periodDate time.Time) (*commonRepo.HppV2FarmDepreciationSnapshotRow, error) {
|
func (s *hppV2RepoStub) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(_ context.Context, projectFlockID uint, periodDate time.Time) (*commonRepo.HppV2FarmDepreciationSnapshotRow, error) {
|
||||||
if s.snapshotByProjectKey == nil {
|
if s.snapshotByProjectKey == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_stocks_project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_recording_stocks_project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE house_depreciation_standards
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_house_depreciation_standards_standard_week_positive;
|
||||||
|
|
||||||
|
ALTER TABLE house_depreciation_standards
|
||||||
|
DROP COLUMN IF EXISTS standard_week;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT NULL;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_recording_stocks_project_flock_kandang_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD CONSTRAINT fk_recording_stocks_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_stocks_project_flock_kandang_id
|
||||||
|
ON recording_stocks(project_flock_kandang_id);
|
||||||
|
|
||||||
|
ALTER TABLE house_depreciation_standards
|
||||||
|
ADD COLUMN IF NOT EXISTS standard_week INT;
|
||||||
|
|
||||||
|
UPDATE house_depreciation_standards
|
||||||
|
SET standard_week = CASE house_type::text
|
||||||
|
WHEN 'close_house' THEN 22
|
||||||
|
WHEN 'open_house' THEN 25
|
||||||
|
ELSE standard_week
|
||||||
|
END
|
||||||
|
WHERE standard_week IS NULL OR standard_week <= 0;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'chk_house_depreciation_standards_standard_week_positive'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE house_depreciation_standards
|
||||||
|
ADD CONSTRAINT chk_house_depreciation_standards_standard_week_positive
|
||||||
|
CHECK (standard_week > 0);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE house_depreciation_standards
|
||||||
|
ALTER COLUMN standard_week SET NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -6,6 +6,7 @@ type HouseDepreciationStandard struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
HouseType string `gorm:"type:house_type_enum;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:1"`
|
HouseType string `gorm:"type:house_type_enum;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:1"`
|
||||||
DayNumber int `gorm:"column:day;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:2"`
|
DayNumber int `gorm:"column:day;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:2"`
|
||||||
|
StandardWeek int `gorm:"column:standard_week;not null"`
|
||||||
DepreciationPercent float64 `gorm:"type:numeric(15,6);not null"`
|
DepreciationPercent float64 `gorm:"type:numeric(15,6);not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
type RecordingStock struct {
|
type RecordingStock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
UsageQty *float64 `gorm:"column:usage_qty"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id;index"`
|
||||||
PendingQty *float64 `gorm:"column:pending_qty"`
|
UsageQty *float64 `gorm:"column:usage_qty"`
|
||||||
|
PendingQty *float64 `gorm:"column:pending_qty"`
|
||||||
|
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ func sapronakIncomingPurchaseQueryParts(params SapronakQueryParams) (string, []a
|
|||||||
fifo.StockableKeyPurchaseItems.String(),
|
fifo.StockableKeyPurchaseItems.String(),
|
||||||
entity.StockAllocationStatusActive,
|
entity.StockAllocationStatusActive,
|
||||||
entity.StockAllocationPurposeConsume,
|
entity.StockAllocationPurposeConsume,
|
||||||
|
fifo.UsableKeyRecordingStock.String(),
|
||||||
|
params.ProjectFlockKandangIDs,
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
params.ProjectFlockKandangIDs,
|
params.ProjectFlockKandangIDs,
|
||||||
params.ProjectFlockKandangIDs,
|
params.ProjectFlockKandangIDs,
|
||||||
params.WarehouseIDs,
|
params.WarehouseIDs,
|
||||||
@@ -323,7 +326,7 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
|
|||||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||||
Where("rec.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
Where("f.name = ?", "PAKAN").
|
Where("f.name = ?", "PAKAN").
|
||||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||||
Scan(&usageAgg).Error
|
Scan(&usageAgg).Error
|
||||||
@@ -905,7 +908,11 @@ WITH scoped_farm_allocations AS (
|
|||||||
WHERE sa.stockable_type = ?
|
WHERE sa.stockable_type = ?
|
||||||
AND sa.status = ?
|
AND sa.status = ?
|
||||||
AND sa.allocation_purpose = ?
|
AND sa.allocation_purpose = ?
|
||||||
AND COALESCE(rec.project_flock_kandangs_id, pc.project_flock_kandang_id) IN ?
|
AND (
|
||||||
|
(sa.usable_type = ? AND rs.project_flock_kandang_id IN ?)
|
||||||
|
OR
|
||||||
|
(sa.usable_type = ? AND pc.project_flock_kandang_id IN ?)
|
||||||
|
)
|
||||||
GROUP BY sa.stockable_id
|
GROUP BY sa.stockable_id
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -1167,7 +1174,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID ui
|
|||||||
"recording_stocks rs",
|
"recording_stocks rs",
|
||||||
"pw.id = rs.product_warehouse_id",
|
"pw.id = rs.product_warehouse_id",
|
||||||
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
"rs.project_flock_kandang_id = ? AND f.name IN ?",
|
||||||
pfkID,
|
pfkID,
|
||||||
sapronakFlagsUsage,
|
sapronakFlagsUsage,
|
||||||
)
|
)
|
||||||
@@ -1208,7 +1215,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, p
|
|||||||
COALESCE(rs.usage_qty,0) AS qty_out,
|
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||||
COALESCE(p.product_price,0) AS price
|
COALESCE(p.product_price,0) AS price
|
||||||
`,
|
`,
|
||||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
"rs.project_flock_kandang_id = ? AND f.name IN ?",
|
||||||
pfkID,
|
pfkID,
|
||||||
sapronakFlagsUsage,
|
sapronakFlagsUsage,
|
||||||
)
|
)
|
||||||
@@ -1294,7 +1301,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
|
|||||||
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
Where(`
|
Where(`
|
||||||
(sa.usable_type = ? AND r.project_flock_kandangs_id = ? AND f.name IN ?)
|
(sa.usable_type = ? AND rs.project_flock_kandang_id = ? AND f.name IN ?)
|
||||||
OR
|
OR
|
||||||
(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ? AND f.name IN ?)
|
(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ? AND f.name IN ?)
|
||||||
`,
|
`,
|
||||||
@@ -1347,7 +1354,12 @@ func (r *ClosingRepositoryImpl) incomingFarmPurchaseAllocationBase(ctx context.C
|
|||||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
Where("w.kandang_id IS NULL").
|
Where("w.kandang_id IS NULL").
|
||||||
Where("COALESCE(rec.project_flock_kandangs_id, pc.project_flock_kandang_id) = ?", projectFlockKandangID).
|
Where("(sa.usable_type = ? AND rs.project_flock_kandang_id = ?) OR (sa.usable_type = ? AND pc.project_flock_kandang_id = ?)",
|
||||||
|
fifo.UsableKeyRecordingStock.String(),
|
||||||
|
projectFlockKandangID,
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
projectFlockKandangID,
|
||||||
|
).
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
Where("pi.received_date IS NOT NULL")
|
Where("pi.received_date IS NOT NULL")
|
||||||
db = applyDateRange(db, "pi.received_date", start, end)
|
db = applyDateRange(db, "pi.received_date", start, end)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func TestSapronakIncomingPurchaseQueryPartsUsesAttributedPurchasesWhenProjectFlo
|
|||||||
if sql != sapronakIncomingPurchasesScopedSQL() {
|
if sql != sapronakIncomingPurchasesScopedSQL() {
|
||||||
t.Fatalf("expected scoped purchase SQL, got %q", sql)
|
t.Fatalf("expected scoped purchase SQL, got %q", sql)
|
||||||
}
|
}
|
||||||
if len(args) != 8 {
|
if len(args) != 11 {
|
||||||
t.Fatalf("expected 8 argument groups, got %d", len(args))
|
t.Fatalf("expected 11 argument groups, got %d", len(args))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ func TestFetchSapronakIncomingIncludesAttributedFarmPurchasesAndHistoricalWareho
|
|||||||
(2, 10, 'products', 'OBAT')`,
|
(2, 10, 'products', 'OBAT')`,
|
||||||
`INSERT INTO purchases (id, po_number, deleted_at) VALUES (1, 'PO-LTI-0005', NULL)`,
|
`INSERT INTO purchases (id, po_number, deleted_at) VALUES (1, 'PO-LTI-0005', NULL)`,
|
||||||
`INSERT INTO recordings (id, project_flock_kandangs_id, deleted_at) VALUES (11, 101, NULL), (12, 999, NULL)`,
|
`INSERT INTO recordings (id, project_flock_kandangs_id, deleted_at) VALUES (11, 101, NULL), (12, 999, NULL)`,
|
||||||
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, usage_qty) VALUES (21, 11, 501, 150), (22, 12, 502, 10)`,
|
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, usage_qty, project_flock_kandang_id) VALUES (21, 11, 501, 150, 101), (22, 12, 502, 10, 999)`,
|
||||||
`INSERT INTO purchase_items (id, purchase_id, product_id, warehouse_id, project_flock_kandang_id, total_qty, price, received_date) VALUES
|
`INSERT INTO purchase_items (id, purchase_id, product_id, warehouse_id, project_flock_kandang_id, total_qty, price, received_date) VALUES
|
||||||
(1, 1, 10, 1, NULL, 100, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
|
(1, 1, 10, 1, NULL, 100, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
|
||||||
(2, 1, 20, 1, NULL, 50, 15000, '` + receivedAt.Format(time.RFC3339) + `'),
|
(2, 1, 20, 1, NULL, 50, 15000, '` + receivedAt.Format(time.RFC3339) + `'),
|
||||||
@@ -145,11 +145,12 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
|
|||||||
deleted_at TIMESTAMP NULL
|
deleted_at TIMESTAMP NULL
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE recording_stocks (
|
`CREATE TABLE recording_stocks (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
recording_id INTEGER NOT NULL,
|
recording_id INTEGER NOT NULL,
|
||||||
product_warehouse_id INTEGER NOT NULL,
|
product_warehouse_id INTEGER NOT NULL,
|
||||||
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0
|
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||||
)`,
|
project_flock_kandang_id INTEGER NULL
|
||||||
|
)`,
|
||||||
`CREATE TABLE project_chickins (
|
`CREATE TABLE project_chickins (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
project_flock_kandang_id INTEGER NOT NULL
|
project_flock_kandang_id INTEGER NOT NULL
|
||||||
|
|||||||
@@ -312,10 +312,10 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
||||||
dtoResult.Warehouse = &mapped
|
dtoResult.Warehouse = &mapped
|
||||||
}
|
}
|
||||||
if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferStateAtDate(c, result.Id, recordDate); serr != nil {
|
if _, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferStateAtDate(c, result.Id, recordDate); serr != nil {
|
||||||
return serr
|
return serr
|
||||||
} else {
|
} else {
|
||||||
dtoResult.IsTransition = isTransition
|
dtoResult.IsTransition = false
|
||||||
dtoResult.IsLaying = isLaying
|
dtoResult.IsLaying = isLaying
|
||||||
}
|
}
|
||||||
applyCutOverLayingLookupOverride(&dtoResult)
|
applyCutOverLayingLookupOverride(&dtoResult)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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 {
|
||||||
@@ -365,6 +366,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stockOwnerProjectFlockKandangID *uint
|
||||||
|
if len(req.Stocks) > 0 {
|
||||||
|
stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfk, recordTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime)
|
day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -441,12 +450,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
mappedStocks := recordingutil.MapStocks(createdRecording.Id, stockOwnerProjectFlockKandangID, req.Stocks)
|
||||||
stockDesired := resetStockQuantitiesForFIFO(mappedStocks)
|
stockDesired := resetStockQuantitiesForFIFO(mappedStocks)
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range mappedStocks {
|
for i := range mappedStocks {
|
||||||
if i >= len(stockDesired) {
|
if i >= len(stockDesired) {
|
||||||
@@ -510,10 +519,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
s.Log.Errorf("Failed to compute recording metrics: %+v", err)
|
s.Log.Errorf("Failed to compute recording metrics: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
||||||
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 {
|
||||||
@@ -574,8 +587,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingEntity = recording
|
recordingEntity = recording
|
||||||
pfkForRoute := recordingEntity.ProjectFlockKandang
|
pfkForRoute := recordingEntity.ProjectFlockKandang
|
||||||
if pfkForRoute == nil || pfkForRoute.Id == 0 {
|
if pfkForRoute == nil || pfkForRoute.Id == 0 {
|
||||||
fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
|
fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
|
||||||
if fetchErr != nil {
|
if fetchErr != nil {
|
||||||
@@ -586,35 +599,43 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return fetchErr
|
return fetchErr
|
||||||
}
|
}
|
||||||
pfkForRoute = fetchedPfk
|
pfkForRoute = fetchedPfk
|
||||||
}
|
}
|
||||||
routePayload := buildRecordingRoutePayloadFromUpdate(req)
|
if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfkForRoute, recordingEntity.RecordDatetime); err != nil {
|
||||||
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
routePayload := buildRecordingRoutePayloadFromUpdate(req)
|
||||||
|
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
hasStockChanges := req.Stocks != nil
|
hasStockChanges := req.Stocks != nil
|
||||||
hasDepletionChanges := req.Depletions != nil
|
hasDepletionChanges := req.Depletions != nil
|
||||||
hasEggChanges := req.Eggs != nil
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
var existingStocks []entity.RecordingStock
|
var existingStocks []entity.RecordingStock
|
||||||
var existingDepletions []entity.RecordingDepletion
|
var existingDepletions []entity.RecordingDepletion
|
||||||
var existingEggs []entity.RecordingEgg
|
var existingEggs []entity.RecordingEgg
|
||||||
var mappedDepletions []entity.RecordingDepletion
|
var mappedDepletions []entity.RecordingDepletion
|
||||||
|
var stockOwnerProjectFlockKandangID *uint
|
||||||
|
|
||||||
note := recordingutil.RecordingNote("Edit", recordingEntity.Id)
|
note := recordingutil.RecordingNote("Edit", recordingEntity.Id)
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfkForRoute, recordingEntity.RecordDatetime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
return err
|
||||||
return err
|
}
|
||||||
}
|
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
existingUsage := recordingutil.StockUsageByWarehouse(existingStocks)
|
existingUsage := recordingutil.StockUsageByWarehouse(existingStocks)
|
||||||
incomingUsage := recordingutil.StockUsageByWarehouseReq(req.Stocks)
|
incomingUsage := recordingutil.StockUsageByWarehouseReq(req.Stocks)
|
||||||
match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage)
|
match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage) && recordingStocksAllOwnedBy(existingStocks, stockOwnerProjectFlockKandangID)
|
||||||
if match {
|
if match {
|
||||||
hasStockChanges = false
|
hasStockChanges = false
|
||||||
} else {
|
} else {
|
||||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -622,11 +643,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
|
if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil {
|
if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, stockOwnerProjectFlockKandangID, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if hasDepletionChanges {
|
if hasDepletionChanges {
|
||||||
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
|
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||||
@@ -788,16 +809,22 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
||||||
s.Log.Errorf("Failed to recalculate recordings after update: %+v", err)
|
s.Log.Errorf("Failed to recalculate recordings after update: %+v", err)
|
||||||
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
|
||||||
@@ -1055,11 +1082,15 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
||||||
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
|
||||||
}
|
}
|
||||||
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
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)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -1435,12 +1466,13 @@ func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
businessDate := recordDate
|
||||||
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||||
if physicalMoveDate.IsZero() || recordDate.Before(physicalMoveDate) {
|
if !physicalMoveDate.IsZero() && businessDate.Before(physicalMoveDate) {
|
||||||
return nil
|
businessDate = physicalMoveDate
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, recordDate); err != nil {
|
if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, businessDate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1453,102 +1485,10 @@ func (s *recordingService) enforceTransferRecordingRoute(
|
|||||||
recordTime time.Time,
|
recordTime time.Time,
|
||||||
payload recordingRoutePayload,
|
payload recordingRoutePayload,
|
||||||
) error {
|
) error {
|
||||||
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil {
|
_ = ctx
|
||||||
return nil
|
_ = pfk
|
||||||
}
|
_ = recordTime
|
||||||
|
_ = payload
|
||||||
recordDate := normalizeDateOnlyUTC(recordTime)
|
|
||||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
|
||||||
|
|
||||||
switch category {
|
|
||||||
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
|
||||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to resolve approved transfer by target kandang %d: %+v", pfk.Id, err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
|
||||||
}
|
|
||||||
|
|
||||||
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
|
||||||
if physicalMoveDate.IsZero() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordDate.Before(physicalMoveDate) {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Recording kandang laying hanya bisa dimulai pada %s (tanggal pindah fisik). Sebelumnya gunakan kandang growing", physicalMoveDate.Format("2006-01-02")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Transfer laying %s dengan tanggal pindah fisik %s belum dieksekusi. Eksekusi transfer terlebih dahulu", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordDate.Before(economicCutoffDate) && payload.StockCount > 0 {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Periode transisi transfer laying %s (%s s.d. %s): input PAKAN/OVK harus dicatat di kandang growing hingga %s. Recording kandang laying pada periode ini hanya untuk deplesi (dan telur bila ada).",
|
|
||||||
transfer.TransferNumber,
|
|
||||||
physicalMoveDate.Format("2006-01-02"),
|
|
||||||
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
|
||||||
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
|
||||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, pfk.Id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to resolve approved transfer by source kandang %d: %+v", pfk.Id, err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
|
||||||
}
|
|
||||||
|
|
||||||
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
|
||||||
if physicalMoveDate.IsZero() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordDate.Before(physicalMoveDate) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Transfer laying %s sudah memasuki tanggal pindah fisik %s namun belum dieksekusi. Eksekusi transfer lalu lakukan recording transisi sesuai aturan", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !recordDate.Before(economicCutoffDate) {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Recording kandang growing hanya diperbolehkan sampai %s. Gunakan kandang laying mulai %s", economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"), economicCutoffDate.Format("2006-01-02")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.DepletionCount > 0 {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Periode transisi transfer laying %s (%s s.d. %s): deplesi harus dicatat di kandang laying tujuan agar mapping tidak ambigu. Kandang growing pada periode ini hanya untuk PAKAN/OVK.",
|
|
||||||
transfer.TransferNumber,
|
|
||||||
physicalMoveDate.Format("2006-01-02"),
|
|
||||||
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1658,6 +1598,362 @@ func boolPtr(value bool) *bool {
|
|||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordingStocksAllOwnedBy(stocks []entity.RecordingStock, owner *uint) bool {
|
||||||
|
for _, stock := range stocks {
|
||||||
|
if !uintPtrEqual(stock.ProjectFlockKandangId, owner) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func uintPtrEqual(a *uint, b *uint) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *a == *b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resolveRecordingStockOwnerProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
pfk *entity.ProjectFlockKandang,
|
||||||
|
recordTime time.Time,
|
||||||
|
) (*uint, error) {
|
||||||
|
if pfk == nil || pfk.Id == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
if category == "" {
|
||||||
|
loaded, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, pfk.Id)
|
||||||
|
if err == nil && loaded != nil {
|
||||||
|
pfk = loaded
|
||||||
|
category = strings.ToUpper(strings.TrimSpace(loaded.ProjectFlock.Category))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
owner := pfk.Id
|
||||||
|
return &owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TransferLayingRepo == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve transfer laying for recording stock owner (target_pfk=%d): %+v", pfk.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan atribusi recording stock")
|
||||||
|
}
|
||||||
|
if transfer == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceProjectFlockKandangID, err := s.resolveTransferSourceProjectFlockKandangID(ctx, transfer)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve transfer source kandang for transfer %d: %+v", transfer.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan sumber kandang growing")
|
||||||
|
}
|
||||||
|
if sourceProjectFlockKandangID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceChickinDate, err := s.getEarliestChickInDateByProjectFlockKandangID(ctx, sourceProjectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve earliest chick-in date for source kandang %d: %+v", sourceProjectFlockKandangID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan umur ayam dari growing")
|
||||||
|
}
|
||||||
|
if sourceChickinDate == nil || sourceChickinDate.IsZero() {
|
||||||
|
owner := sourceProjectFlockKandangID
|
||||||
|
return &owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholdDay, err := s.resolveLayingDepreciationThresholdDay(ctx, pfk)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve laying threshold day for kandang %d: %+v", pfk.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan standar umur laying")
|
||||||
|
}
|
||||||
|
if thresholdDay <= 0 {
|
||||||
|
thresholdDay = commonSvc.DepreciationStartAgeDay(resolveHouseType(pfk))
|
||||||
|
}
|
||||||
|
if thresholdDay <= 0 {
|
||||||
|
thresholdDay = commonSvc.DepreciationStartAgeDay("close_house")
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDate := normalizeDateOnlyUTC(recordTime)
|
||||||
|
chickinDate := normalizeDateOnlyUTC(*sourceChickinDate)
|
||||||
|
ageDay := commonSvc.FlockAgeDay(chickinDate, recordDate)
|
||||||
|
if ageDay < thresholdDay {
|
||||||
|
owner := sourceProjectFlockKandangID
|
||||||
|
return &owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
owner := pfk.Id
|
||||||
|
return &owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveHouseType(pfk *entity.ProjectFlockKandang) string {
|
||||||
|
if pfk == nil || pfk.Kandang.HouseType == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(*pfk.Kandang.HouseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resolveLayingDepreciationThresholdDay(ctx context.Context, pfk *entity.ProjectFlockKandang) (int, error) {
|
||||||
|
houseType := commonSvc.NormalizeDepreciationHouseType(resolveHouseType(pfk))
|
||||||
|
if houseType == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var row struct {
|
||||||
|
StandardWeek int `gorm:"column:standard_week"`
|
||||||
|
}
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Table("house_depreciation_standards").
|
||||||
|
Select("standard_week").
|
||||||
|
Where("house_type::text = ?", houseType).
|
||||||
|
Where("standard_week > 0").
|
||||||
|
Order("effective_date DESC NULLS LAST").
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if row.StandardWeek <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return (row.StandardWeek * 7) + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resolveTransferSourceProjectFlockKandangID(ctx context.Context, transfer *entity.LayingTransfer) (uint, error) {
|
||||||
|
if transfer == nil || transfer.Id == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var row struct {
|
||||||
|
SourceProjectFlockKandangID uint `gorm:"column:source_project_flock_kandang_id"`
|
||||||
|
}
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Table("laying_transfer_sources").
|
||||||
|
Select("source_project_flock_kandang_id").
|
||||||
|
Where("laying_transfer_id = ?", transfer.Id).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Where("source_project_flock_kandang_id > 0").
|
||||||
|
Order("id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&row).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
|
||||||
|
return *transfer.SourceProjectFlockKandangId, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if row.SourceProjectFlockKandangID != 0 {
|
||||||
|
return row.SourceProjectFlockKandangID, nil
|
||||||
|
}
|
||||||
|
if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
|
||||||
|
return *transfer.SourceProjectFlockKandangId, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*time.Time, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var row struct {
|
||||||
|
ChickInDate *time.Time `gorm:"column:chick_in_date"`
|
||||||
|
}
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Table("project_chickins").
|
||||||
|
Select("MIN(chick_in_date) AS chick_in_date").
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Scan(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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,
|
||||||
@@ -2588,6 +2884,7 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
|||||||
recordingID uint,
|
recordingID uint,
|
||||||
existing []entity.RecordingStock,
|
existing []entity.RecordingStock,
|
||||||
incoming []validation.Stock,
|
incoming []validation.Stock,
|
||||||
|
ownerProjectFlockKandangID *uint,
|
||||||
note string,
|
note string,
|
||||||
actorID uint,
|
actorID uint,
|
||||||
) error {
|
) error {
|
||||||
@@ -2608,21 +2905,32 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
|||||||
if len(list) > 0 {
|
if len(list) > 0 {
|
||||||
stock = list[0]
|
stock = list[0]
|
||||||
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
||||||
} else {
|
} else {
|
||||||
zero := 0.0
|
zero := 0.0
|
||||||
stock = entity.RecordingStock{
|
stock = entity.RecordingStock{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: item.ProductWarehouseId,
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
UsageQty: &zero,
|
ProjectFlockKandangId: ownerProjectFlockKandangID,
|
||||||
PendingQty: &zero,
|
UsageQty: &zero,
|
||||||
|
PendingQty: &zero,
|
||||||
|
}
|
||||||
|
if err := s.Repository.CreateStock(tx, &stock); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := s.Repository.CreateStock(tx, &stock); err != nil {
|
stock.ProjectFlockKandangId = ownerProjectFlockKandangID
|
||||||
return err
|
if stock.Id != 0 {
|
||||||
|
if err := tx.Model(&entity.RecordingStock{}).
|
||||||
|
Where("id = ?", stock.Id).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"project_flock_kandang_id": ownerProjectFlockKandangID,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
desired := item.Qty
|
desired := item.Qty
|
||||||
stock.UsageQty = &desired
|
stock.UsageQty = &desired
|
||||||
zero := 0.0
|
zero := 0.0
|
||||||
stock.PendingQty = &zero
|
stock.PendingQty = &zero
|
||||||
stocksToApply = append(stocksToApply, stock)
|
stocksToApply = append(stocksToApply, stock)
|
||||||
|
|||||||
@@ -148,6 +148,53 @@ func TestShouldNormalizeEggRequestsOnUpdateNormalizesFarmLevelEggs(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveTransferSourceProjectFlockKandangIDPrefersTransferResourceSource(t *testing.T) {
|
||||||
|
db := setupTransferSourceResolutionTestDB(t)
|
||||||
|
repo := repository.NewRecordingRepository(db)
|
||||||
|
svc := &recordingService{Repository: repo}
|
||||||
|
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
Id: 77,
|
||||||
|
SourceProjectFlockKandangId: uintPtrForTest(999),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Exec(`INSERT INTO laying_transfer_sources (id, laying_transfer_id, source_project_flock_kandang_id, deleted_at) VALUES (1, 77, 123, NULL)`).Error; err != nil {
|
||||||
|
t.Fatalf("failed seeding laying_transfer_sources: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.resolveTransferSourceProjectFlockKandangID(context.Background(), transfer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if got != 123 {
|
||||||
|
t.Fatalf("expected resource source project_flock_kandang_id=123, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveTransferSourceProjectFlockKandangIDFallsBackToTransferHeader(t *testing.T) {
|
||||||
|
db := setupTransferSourceResolutionTestDB(t)
|
||||||
|
repo := repository.NewRecordingRepository(db)
|
||||||
|
svc := &recordingService{Repository: repo}
|
||||||
|
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
Id: 78,
|
||||||
|
SourceProjectFlockKandangId: uintPtrForTest(456),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.resolveTransferSourceProjectFlockKandangID(context.Background(), transfer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if got != 456 {
|
||||||
|
t.Fatalf("expected fallback source project_flock_kandang_id=456, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uintPtrForTest(value uint) *uint {
|
||||||
|
v := value
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func setupRecordingServiceTestDB(t *testing.T) *gorm.DB {
|
func setupRecordingServiceTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -193,3 +240,23 @@ func setupRecordingServiceTestDB(t *testing.T) *gorm.DB {
|
|||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupTransferSourceResolutionTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed opening sqlite db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Exec(`CREATE TABLE laying_transfer_sources (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
laying_transfer_id INTEGER NOT NULL,
|
||||||
|
source_project_flock_kandang_id INTEGER NOT NULL,
|
||||||
|
deleted_at TIMESTAMP NULL
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("failed creating laying_transfer_sources schema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|||||||
@@ -231,6 +231,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
transferDate, err := utils.ParseDateString(req.TransferDate)
|
transferDate, err := utils.ParseDateString(req.TransferDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -451,6 +454,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
transferDate, err := time.Parse("2006-01-02", req.TransferDate)
|
transferDate, err := time.Parse("2006-01-02", req.TransferDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1611,6 +1617,80 @@ func (s *transferLayingService) validateKandangOwnership(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) validateTargetSourceLineage(
|
||||||
|
ctx context.Context,
|
||||||
|
sourceProjectFlockKandangID uint,
|
||||||
|
targetKandangIDs []uint,
|
||||||
|
excludeTransferID uint,
|
||||||
|
) error {
|
||||||
|
if sourceProjectFlockKandangID == 0 || len(targetKandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{}, len(targetKandangIDs))
|
||||||
|
for _, targetKandangID := range targetKandangIDs {
|
||||||
|
if targetKandangID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[targetKandangID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[targetKandangID] = struct{}{}
|
||||||
|
|
||||||
|
existingTransfer, err := s.Repository.GetLatestApprovedByTargetKandang(ctx, targetKandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate transfer lineage for target kandang %d: %+v", targetKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
|
||||||
|
}
|
||||||
|
if existingTransfer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if excludeTransferID != 0 && existingTransfer.Id == excludeTransferID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
existingSourceID := uint(0)
|
||||||
|
if existingTransfer.SourceProjectFlockKandangId != nil && *existingTransfer.SourceProjectFlockKandangId != 0 {
|
||||||
|
existingSourceID = *existingTransfer.SourceProjectFlockKandangId
|
||||||
|
}
|
||||||
|
if existingSourceID == 0 && s.LayingTransferSourceRepo != nil {
|
||||||
|
sources, sourceErr := s.LayingTransferSourceRepo.GetByLayingTransferId(ctx, existingTransfer.Id)
|
||||||
|
if sourceErr != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve transfer sources for lineage validation transfer=%d: %+v", existingTransfer.Id, sourceErr)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
|
||||||
|
}
|
||||||
|
for _, source := range sources {
|
||||||
|
if source.SourceProjectFlockKandangId != 0 {
|
||||||
|
existingSourceID = source.SourceProjectFlockKandangId
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existingSourceID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if existingSourceID == sourceProjectFlockKandangID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Kandang tujuan %d sudah memiliki lineage sumber kandang %d dari transfer %s. Tidak boleh ganti ke sumber kandang %d.",
|
||||||
|
targetKandangID,
|
||||||
|
existingSourceID,
|
||||||
|
existingTransfer.TransferNumber,
|
||||||
|
sourceProjectFlockKandangID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {
|
func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
feedQuery := r.db.WithContext(ctx).
|
feedQuery := r.db.WithContext(ctx).
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select(`
|
Select(`
|
||||||
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
rs.project_flock_kandang_id AS project_flock_kandang_id,
|
||||||
COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0) AS feed_cost,
|
COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0) AS feed_cost,
|
||||||
s.id AS supplier_id,
|
s.id AS supplier_id,
|
||||||
s.name AS supplier_name,
|
s.name AS supplier_name,
|
||||||
@@ -233,10 +233,10 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
Where("rs.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
Where("f.name = ?", utils.FlagPakan).
|
Where("f.name = ?", utils.FlagPakan).
|
||||||
Group("r.project_flock_kandangs_id, s.id, s.name, s.alias")
|
Group("rs.project_flock_kandang_id, s.id, s.name, s.alias")
|
||||||
|
|
||||||
if err := feedQuery.Scan(&feedRows).Error; err != nil {
|
if err := feedQuery.Scan(&feedRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
@@ -21,12 +22,17 @@ import (
|
|||||||
|
|
||||||
type expenseDepreciationRepoMock struct {
|
type expenseDepreciationRepoMock struct {
|
||||||
repportRepo.ExpenseDepreciationRepository
|
repportRepo.ExpenseDepreciationRepository
|
||||||
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
||||||
|
candidateRows []repportRepo.FarmDepreciationCandidateRow
|
||||||
|
snapshots []entity.FarmDepreciationSnapshot
|
||||||
|
|
||||||
upsertedRow *entity.FarmDepreciationManualInput
|
upsertedRow *entity.FarmDepreciationManualInput
|
||||||
deleteCalled bool
|
deleteCalled bool
|
||||||
deleteDate time.Time
|
deleteDate time.Time
|
||||||
deleteFarmIDs []uint
|
deleteFarmIDs []uint
|
||||||
|
upsertSnapshotCalls int
|
||||||
|
upsertedSnapshots []entity.FarmDepreciationSnapshot
|
||||||
|
getSnapshotsCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
|
func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
|
||||||
@@ -46,6 +52,37 @@ func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *expenseDepreciationRepoMock) GetCandidateFarms(_ context.Context, _ time.Time, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationCandidateRow, error) {
|
||||||
|
return append([]repportRepo.FarmDepreciationCandidateRow{}, m.candidateRows...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *expenseDepreciationRepoMock) GetSnapshotsByPeriodAndFarmIDs(_ context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) {
|
||||||
|
m.getSnapshotsCalls++
|
||||||
|
if len(farmIDs) == 0 {
|
||||||
|
return []entity.FarmDepreciationSnapshot{}, nil
|
||||||
|
}
|
||||||
|
allowed := make(map[uint]struct{}, len(farmIDs))
|
||||||
|
for _, farmID := range farmIDs {
|
||||||
|
allowed[farmID] = struct{}{}
|
||||||
|
}
|
||||||
|
result := make([]entity.FarmDepreciationSnapshot, 0, len(m.snapshots))
|
||||||
|
for _, row := range m.snapshots {
|
||||||
|
if _, ok := allowed[row.ProjectFlockId]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if row.PeriodDate.IsZero() || row.PeriodDate.Format("2006-01-02") == period.Format("2006-01-02") {
|
||||||
|
result = append(result, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *expenseDepreciationRepoMock) UpsertSnapshots(_ context.Context, rows []entity.FarmDepreciationSnapshot) error {
|
||||||
|
m.upsertSnapshotCalls++
|
||||||
|
m.upsertedSnapshots = append([]entity.FarmDepreciationSnapshot{}, rows...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error {
|
func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error {
|
||||||
m.deleteCalled = true
|
m.deleteCalled = true
|
||||||
m.deleteDate = fromDate
|
m.deleteDate = fromDate
|
||||||
@@ -57,6 +94,15 @@ func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Con
|
|||||||
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type expenseRealizationRepoMock struct {
|
||||||
|
expenseRepo.ExpenseRealizationRepository
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *expenseRealizationRepoMock) DB() *gorm.DB {
|
||||||
|
return m.db
|
||||||
|
}
|
||||||
|
|
||||||
type hppCostRepoMock struct {
|
type hppCostRepoMock struct {
|
||||||
commonRepo.HppCostRepository
|
commonRepo.HppCostRepository
|
||||||
kandangIDsByFarm map[uint][]uint
|
kandangIDsByFarm map[uint][]uint
|
||||||
@@ -352,6 +398,167 @@ func TestComputeExpenseDepreciationSnapshots_ZeroWhenDepreciationMissing(t *test
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetExpenseDepreciation_UsesExistingSnapshotWhenForceRecomputeFalse(t *testing.T) {
|
||||||
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
||||||
|
repo := &expenseDepreciationRepoMock{
|
||||||
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
||||||
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
||||||
|
},
|
||||||
|
snapshots: []entity.FarmDepreciationSnapshot{
|
||||||
|
{
|
||||||
|
ProjectFlockId: 1,
|
||||||
|
PeriodDate: periodDate,
|
||||||
|
DepreciationPercentEffective: 11.1,
|
||||||
|
DepreciationValue: 111,
|
||||||
|
PulletCostDayNTotal: 1001,
|
||||||
|
Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &repportService{
|
||||||
|
Validate: validator.New(),
|
||||||
|
ExpenseDepreciationRepo: repo,
|
||||||
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
||||||
|
HppCostRepo: &hppCostRepoMock{},
|
||||||
|
HppV2Svc: &hppV2ServiceMock{},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, meta, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].DepreciationValue != 111 {
|
||||||
|
t.Fatalf("expected depreciation value 111, got %v", rows[0].DepreciationValue)
|
||||||
|
}
|
||||||
|
if meta == nil || meta.TotalResults != 1 {
|
||||||
|
t.Fatalf("expected meta total_results 1, got %+v", meta)
|
||||||
|
}
|
||||||
|
if repo.upsertSnapshotCalls != 0 {
|
||||||
|
t.Fatalf("expected no snapshot upsert, got %d", repo.upsertSnapshotCalls)
|
||||||
|
}
|
||||||
|
if repo.getSnapshotsCalls != 1 {
|
||||||
|
t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExpenseDepreciation_ForceRecomputeRebuildsAllSnapshots(t *testing.T) {
|
||||||
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
||||||
|
repo := &expenseDepreciationRepoMock{
|
||||||
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
||||||
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
||||||
|
},
|
||||||
|
snapshots: []entity.FarmDepreciationSnapshot{
|
||||||
|
{
|
||||||
|
ProjectFlockId: 1,
|
||||||
|
PeriodDate: periodDate,
|
||||||
|
DepreciationValue: 999,
|
||||||
|
PulletCostDayNTotal: 999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &repportService{
|
||||||
|
Validate: validator.New(),
|
||||||
|
ExpenseDepreciationRepo: repo,
|
||||||
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
||||||
|
HppCostRepo: &hppCostRepoMock{
|
||||||
|
kandangIDsByFarm: map[uint][]uint{
|
||||||
|
1: {10},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HppV2Svc: &hppV2ServiceMock{
|
||||||
|
breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
|
||||||
|
10: depreciationBreakdown(10, 100, "Kandang A", 100, 1000, 10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05&force_recompute=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].DepreciationValue != 100 {
|
||||||
|
t.Fatalf("expected recomputed depreciation value 100, got %v", rows[0].DepreciationValue)
|
||||||
|
}
|
||||||
|
if repo.upsertSnapshotCalls != 1 {
|
||||||
|
t.Fatalf("expected snapshot upsert called once, got %d", repo.upsertSnapshotCalls)
|
||||||
|
}
|
||||||
|
if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 1 {
|
||||||
|
t.Fatalf("expected upserted snapshot for farm 1, got %+v", repo.upsertedSnapshots)
|
||||||
|
}
|
||||||
|
if repo.getSnapshotsCalls != 0 {
|
||||||
|
t.Fatalf("expected no snapshot fetch in force recompute mode, got %d", repo.getSnapshotsCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExpenseDepreciation_ForceRecomputeFalseComputesOnlyMissingFarms(t *testing.T) {
|
||||||
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
||||||
|
repo := &expenseDepreciationRepoMock{
|
||||||
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
||||||
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
||||||
|
{ProjectFlockID: 2, FarmName: "Farm B"},
|
||||||
|
},
|
||||||
|
snapshots: []entity.FarmDepreciationSnapshot{
|
||||||
|
{
|
||||||
|
ProjectFlockId: 1,
|
||||||
|
PeriodDate: periodDate,
|
||||||
|
DepreciationPercentEffective: 11.1,
|
||||||
|
DepreciationValue: 111,
|
||||||
|
PulletCostDayNTotal: 1001,
|
||||||
|
Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &repportService{
|
||||||
|
Validate: validator.New(),
|
||||||
|
ExpenseDepreciationRepo: repo,
|
||||||
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
||||||
|
HppCostRepo: &hppCostRepoMock{
|
||||||
|
kandangIDsByFarm: map[uint][]uint{
|
||||||
|
1: {10},
|
||||||
|
2: {20},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HppV2Svc: &hppV2ServiceMock{
|
||||||
|
breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
|
||||||
|
10: depreciationBreakdown(10, 100, "Kandang A", 999, 9999, 10),
|
||||||
|
20: depreciationBreakdown(20, 200, "Kandang B", 200, 2000, 10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 2 {
|
||||||
|
t.Fatalf("expected 2 rows, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].ProjectFlockID != 1 || rows[0].DepreciationValue != 111 {
|
||||||
|
t.Fatalf("expected farm 1 use existing snapshot value 111, got %+v", rows[0])
|
||||||
|
}
|
||||||
|
if rows[1].ProjectFlockID != 2 || rows[1].DepreciationValue != 200 {
|
||||||
|
t.Fatalf("expected farm 2 recomputed value 200, got %+v", rows[1])
|
||||||
|
}
|
||||||
|
if repo.upsertSnapshotCalls != 1 {
|
||||||
|
t.Fatalf("expected one upsert call for missing farms, got %d", repo.upsertSnapshotCalls)
|
||||||
|
}
|
||||||
|
if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 2 {
|
||||||
|
t.Fatalf("expected upsert only farm 2 snapshot, got %+v", repo.upsertedSnapshots)
|
||||||
|
}
|
||||||
|
if repo.getSnapshotsCalls != 1 {
|
||||||
|
t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) {
|
func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) {
|
||||||
repo := &expenseDepreciationRepoMock{
|
repo := &expenseDepreciationRepoMock{
|
||||||
manualInputs: []repportRepo.FarmDepreciationManualInputRow{
|
manualInputs: []repportRepo.FarmDepreciationManualInputRow{
|
||||||
@@ -411,6 +618,82 @@ func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getExpenseDepreciationByQuery(t *testing.T, svc *repportService, query string) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
var (
|
||||||
|
rows []dto.ExpenseDepreciationRowDTO
|
||||||
|
meta *dto.ExpenseDepreciationMetaDTO
|
||||||
|
)
|
||||||
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
resultRows, resultMeta, err := svc.GetExpenseDepreciation(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows = resultRows
|
||||||
|
meta = resultMeta
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
target := "/"
|
||||||
|
if query != "" {
|
||||||
|
target += "?" + query
|
||||||
|
}
|
||||||
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, target, nil))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
return nil, nil, fiber.NewError(resp.StatusCode, "request failed")
|
||||||
|
}
|
||||||
|
return rows, meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func depreciationBreakdown(
|
||||||
|
projectFlockKandangID uint,
|
||||||
|
kandangID uint,
|
||||||
|
kandangName string,
|
||||||
|
depreciationValue float64,
|
||||||
|
pulletCostDayN float64,
|
||||||
|
depreciationPercent float64,
|
||||||
|
) *approvalService.HppV2Breakdown {
|
||||||
|
return &approvalService.HppV2Breakdown{
|
||||||
|
ProjectFlockKandangID: projectFlockKandangID,
|
||||||
|
KandangID: kandangID,
|
||||||
|
KandangName: kandangName,
|
||||||
|
HouseType: "close_house",
|
||||||
|
Components: []approvalService.HppV2Component{
|
||||||
|
{
|
||||||
|
Code: "DEPRECIATION",
|
||||||
|
Title: "Depreciation",
|
||||||
|
Total: depreciationValue,
|
||||||
|
Parts: []approvalService.HppV2ComponentPart{
|
||||||
|
{
|
||||||
|
Code: "normal_transfer",
|
||||||
|
Total: depreciationValue,
|
||||||
|
Details: map[string]any{
|
||||||
|
"schedule_day": 1,
|
||||||
|
"depreciation_percent": depreciationPercent,
|
||||||
|
"pullet_cost_day_n": pulletCostDayN,
|
||||||
|
"source_project_flock_id": 77,
|
||||||
|
"origin_date": "2026-01-01",
|
||||||
|
},
|
||||||
|
References: []approvalService.HppV2Reference{
|
||||||
|
{
|
||||||
|
Type: "laying_transfer",
|
||||||
|
ID: 701,
|
||||||
|
Date: "2026-05-20",
|
||||||
|
Qty: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents {
|
func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var out depreciationFarmComponents
|
var out depreciationFarmComponents
|
||||||
|
|||||||
@@ -231,25 +231,9 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe
|
|||||||
farmNameByID[row.ProjectFlockID] = row.FarmName
|
farmNameByID[row.ProjectFlockID] = row.FarmName
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
|
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot)
|
||||||
if err != nil {
|
if params.ForceRecompute {
|
||||||
return nil, nil, err
|
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID)
|
||||||
}
|
|
||||||
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
|
|
||||||
for _, row := range snapshots {
|
|
||||||
snapshotByFarmID[row.ProjectFlockId] = row
|
|
||||||
}
|
|
||||||
|
|
||||||
missingFarmIDs := make([]uint, 0)
|
|
||||||
for _, farmID := range farmIDs {
|
|
||||||
if _, exists := snapshotByFarmID[farmID]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
missingFarmIDs = append(missingFarmIDs, farmID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missingFarmIDs) > 0 {
|
|
||||||
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
|
|
||||||
if computeErr != nil {
|
if computeErr != nil {
|
||||||
return nil, nil, computeErr
|
return nil, nil, computeErr
|
||||||
}
|
}
|
||||||
@@ -257,10 +241,43 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe
|
|||||||
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots))
|
||||||
for _, row := range computedSnapshots {
|
for _, row := range computedSnapshots {
|
||||||
snapshotByFarmID[row.ProjectFlockId] = row
|
snapshotByFarmID[row.ProjectFlockId] = row
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
|
||||||
|
for _, row := range snapshots {
|
||||||
|
snapshotByFarmID[row.ProjectFlockId] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
missingFarmIDs := make([]uint, 0)
|
||||||
|
for _, farmID := range farmIDs {
|
||||||
|
if _, exists := snapshotByFarmID[farmID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missingFarmIDs = append(missingFarmIDs, farmID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingFarmIDs) > 0 {
|
||||||
|
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
|
||||||
|
if computeErr != nil {
|
||||||
|
return nil, nil, computeErr
|
||||||
|
}
|
||||||
|
if len(computedSnapshots) > 0 {
|
||||||
|
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, row := range computedSnapshots {
|
||||||
|
snapshotByFarmID[row.ProjectFlockId] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
|
rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
|
||||||
@@ -2717,6 +2734,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
|||||||
rawLocation := ctx.Query("location_id", "")
|
rawLocation := ctx.Query("location_id", "")
|
||||||
rawProjectFlock := ctx.Query("project_flock_id", "")
|
rawProjectFlock := ctx.Query("project_flock_id", "")
|
||||||
period := strings.TrimSpace(ctx.Query("period", ""))
|
period := strings.TrimSpace(ctx.Query("period", ""))
|
||||||
|
forceRecompute := ctx.QueryBool("force_recompute", false)
|
||||||
|
|
||||||
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
|
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2766,6 +2784,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
|||||||
Page: page,
|
Page: page,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Period: period,
|
Period: period,
|
||||||
|
ForceRecompute: forceRecompute,
|
||||||
ProjectFlockIDs: projectFlockIDs,
|
ProjectFlockIDs: projectFlockIDs,
|
||||||
AreaIDs: areaIDs,
|
AreaIDs: areaIDs,
|
||||||
LocationIDs: locationIDs,
|
LocationIDs: locationIDs,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type ExpenseDepreciationQuery struct {
|
|||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||||
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
||||||
|
ForceRecompute bool `query:"force_recompute"`
|
||||||
ProjectFlockIDs []int64 `query:"-"`
|
ProjectFlockIDs []int64 `query:"-"`
|
||||||
AreaIDs []int64 `query:"-"`
|
AreaIDs []int64 `query:"-"`
|
||||||
LocationIDs []int64 `query:"-"`
|
LocationIDs []int64 `query:"-"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
|
func MapStocks(recordingID uint, ownerProjectFlockKandangID *uint, items []validation.Stock) []entity.RecordingStock {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,10 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
|
|||||||
usagePtr := new(float64)
|
usagePtr := new(float64)
|
||||||
*usagePtr = item.Qty
|
*usagePtr = item.Qty
|
||||||
result = append(result, entity.RecordingStock{
|
result = append(result, entity.RecordingStock{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: item.ProductWarehouseId,
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
UsageQty: usagePtr,
|
ProjectFlockKandangId: ownerProjectFlockKandangID,
|
||||||
|
UsageQty: usagePtr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user