mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
init adjustment recording
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,50 @@ 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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("(rs.project_flock_kandang_id IS NULL OR rs.project_flock_kandang_id <> r.project_flock_kandangs_id)").
|
||||||
|
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 +438,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 +800,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
|
||||||
|
|||||||
@@ -96,6 +96,54 @@ func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments(
|
|||||||
assertFloatEquals(t, totalWeightKg, 1.4)
|
assertFloatEquals(t, totalWeightKg, 1.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) {
|
||||||
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES (101, 1, 1), (201, 2, 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-11 08:00:00', NULL),
|
||||||
|
(3, 101, '2026-04-12 08:00:00', NULL)`,
|
||||||
|
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES
|
||||||
|
(501, 201, 10, NULL),
|
||||||
|
(502, 201, 11, NULL),
|
||||||
|
(503, 201, 12, NULL)`,
|
||||||
|
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES
|
||||||
|
(10, 'products', 10, 'PAKAN'),
|
||||||
|
(11, 'products', 11, 'OVK'),
|
||||||
|
(12, 'products', 12, '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)`,
|
||||||
|
`INSERT INTO purchase_items (id, product_id, price) VALUES
|
||||||
|
(601, 10, 100),
|
||||||
|
(602, 11, 200),
|
||||||
|
(603, 12, 300)`,
|
||||||
|
`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.5),
|
||||||
|
(9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
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, 500)
|
||||||
|
|
||||||
|
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, 200)
|
||||||
|
}
|
||||||
|
|
||||||
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -111,6 +159,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 +228,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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,356 @@ 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
|
||||||
|
}
|
||||||
|
if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
|
||||||
|
return *transfer.SourceProjectFlockKandangId, 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) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return row.SourceProjectFlockKandangID, 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 +2878,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 +2899,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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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