mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 14:55:42 +00:00
feat: reimplement with plan hppv2 flow and logics
This commit is contained in:
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -10,8 +11,61 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type HppV2ProjectFlockKandangContext struct {
|
||||||
|
ProjectFlockKandangID uint
|
||||||
|
ProjectFlockID uint
|
||||||
|
ProjectFlockCategory string
|
||||||
|
KandangID uint
|
||||||
|
KandangName string
|
||||||
|
LocationID uint
|
||||||
|
HouseType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2UsageCostRow struct {
|
||||||
|
StockableType string
|
||||||
|
StockableID uint
|
||||||
|
SourceProductID uint
|
||||||
|
SourceProductName string
|
||||||
|
Qty float64
|
||||||
|
UnitPrice float64
|
||||||
|
TotalCost float64
|
||||||
|
FirstUsedAt time.Time
|
||||||
|
LastUsedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2AdjustmentCostRow struct {
|
||||||
|
AdjustmentID uint
|
||||||
|
ProjectFlockKandangID *uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
WarehouseID uint
|
||||||
|
WarehouseType string
|
||||||
|
Qty float64
|
||||||
|
Price float64
|
||||||
|
GrandTotal float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2ExpenseCostRow struct {
|
||||||
|
ExpenseRealizationID uint
|
||||||
|
ExpenseNonstockID uint
|
||||||
|
ExpenseID uint
|
||||||
|
NonstockID uint
|
||||||
|
NonstockName string
|
||||||
|
Qty float64
|
||||||
|
Price float64
|
||||||
|
TotalCost float64
|
||||||
|
RealizationDate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type HppV2CostRepository interface {
|
type HppV2CostRepository interface {
|
||||||
|
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
|
||||||
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
|
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
|
||||||
|
ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
|
||||||
|
ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error)
|
||||||
|
ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
||||||
|
ListExpenseRealizationRowsByProjectFlockID(ctx context.Context, projectFlockID uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
||||||
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
|
||||||
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
|
||||||
@@ -27,6 +81,33 @@ func NewHppV2CostRepository(db *gorm.DB) HppV2CostRepository {
|
|||||||
return &HppV2RepositoryImpl{db: db}
|
return &HppV2RepositoryImpl{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error) {
|
||||||
|
var row HppV2ProjectFlockKandangContext
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs AS pfk").
|
||||||
|
Select(`
|
||||||
|
pfk.id AS project_flock_kandang_id,
|
||||||
|
pf.id AS project_flock_id,
|
||||||
|
pf.category AS project_flock_category,
|
||||||
|
k.id AS kandang_id,
|
||||||
|
k.name AS kandang_name,
|
||||||
|
k.location_id AS location_id,
|
||||||
|
k.house_type::text AS house_type
|
||||||
|
`).
|
||||||
|
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Where("pfk.id = ?", projectFlockKandangId).
|
||||||
|
Scan(&row).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if row.ProjectFlockKandangID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) {
|
func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) {
|
||||||
var ids []uint
|
var ids []uint
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
@@ -41,6 +122,241 @@ func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, pro
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockKandangIDs []uint,
|
||||||
|
flagNames []string,
|
||||||
|
date *time.Time,
|
||||||
|
) ([]HppV2UsageCostRow, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||||
|
return []HppV2UsageCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
|
||||||
|
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
|
||||||
|
usableRecordingStock := fifo.UsableKeyRecordingStock.String()
|
||||||
|
|
||||||
|
rows := make([]HppV2UsageCostRow, 0)
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select(`
|
||||||
|
sa.stockable_type AS stockable_type,
|
||||||
|
sa.stockable_id AS stockable_id,
|
||||||
|
COALESCE(pi.product_id, ast_pw.product_id, 0) AS source_product_id,
|
||||||
|
COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name,
|
||||||
|
COALESCE(SUM(sa.qty), 0) AS qty,
|
||||||
|
CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END AS unit_price,
|
||||||
|
COALESCE(SUM(sa.qty * CASE
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END), 0) AS total_cost,
|
||||||
|
MIN(r.record_datetime) AS first_used_at,
|
||||||
|
MAX(r.record_datetime) AS last_used_at
|
||||||
|
`,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
).
|
||||||
|
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.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 = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||||
|
usableRecordingStock,
|
||||||
|
stockablePurchase,
|
||||||
|
stockableAdjustment,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
|
||||||
|
Joins("LEFT JOIN products AS pi_prod ON pi_prod.id = pi.product_id").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
|
||||||
|
Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id").
|
||||||
|
Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id").
|
||||||
|
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||||
|
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).
|
||||||
|
Group(`
|
||||||
|
sa.stockable_type,
|
||||||
|
sa.stockable_id,
|
||||||
|
COALESCE(pi.product_id, ast_pw.product_id, 0),
|
||||||
|
COALESCE(pi_prod.name, ast_prod.name, ''),
|
||||||
|
CASE
|
||||||
|
WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0)
|
||||||
|
WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
`).
|
||||||
|
Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockKandangIDs []uint,
|
||||||
|
flagNames []string,
|
||||||
|
date *time.Time,
|
||||||
|
) ([]HppV2AdjustmentCostRow, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||||
|
return []HppV2AdjustmentCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]HppV2AdjustmentCostRow, 0)
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("adjustment_stocks AS ast").
|
||||||
|
Select(`
|
||||||
|
ast.id AS adjustment_id,
|
||||||
|
pw.project_flock_kandang_id AS project_flock_kandang_id,
|
||||||
|
ast.product_warehouse_id AS product_warehouse_id,
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
w.id AS warehouse_id,
|
||||||
|
w.type AS warehouse_type,
|
||||||
|
COALESCE(ast.total_qty, 0) AS qty,
|
||||||
|
COALESCE(ast.price, 0) AS price,
|
||||||
|
COALESCE(ast.grand_total, 0) AS grand_total,
|
||||||
|
ast.created_at AS created_at
|
||||||
|
`).
|
||||||
|
Joins("JOIN product_warehouses AS pw ON pw.id = ast.product_warehouse_id").
|
||||||
|
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("ast.created_at <= ?", *date).
|
||||||
|
Where("COALESCE(ast.total_qty, 0) > 0").
|
||||||
|
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).
|
||||||
|
Order("ast.created_at ASC, ast.id ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockKandangIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockKandangIDs []uint,
|
||||||
|
date *time.Time,
|
||||||
|
ekspedisi bool,
|
||||||
|
) ([]HppV2ExpenseCostRow, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return []HppV2ExpenseCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]HppV2ExpenseCostRow, 0)
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Table("expense_realizations AS er").
|
||||||
|
Select(`
|
||||||
|
er.id AS expense_realization_id,
|
||||||
|
en.id AS expense_nonstock_id,
|
||||||
|
e.id AS expense_id,
|
||||||
|
COALESCE(n.id, 0) AS nonstock_id,
|
||||||
|
COALESCE(n.name, '') AS nonstock_name,
|
||||||
|
COALESCE(er.qty, 0) AS qty,
|
||||||
|
COALESCE(er.price, 0) AS price,
|
||||||
|
COALESCE(er.qty, 0) * COALESCE(er.price, 0) AS total_cost,
|
||||||
|
COALESCE(e.realization_date, DATE(er.created_at)) AS realization_date
|
||||||
|
`).
|
||||||
|
Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id").
|
||||||
|
Joins("JOIN expenses AS e ON e.id = en.expense_id").
|
||||||
|
Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||||
|
Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi).
|
||||||
|
Where("e.deleted_at IS NULL").
|
||||||
|
Where("e.category = ?", utils.ExpenseCategoryBOP).
|
||||||
|
Where("en.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("COALESCE(e.realization_date, DATE(er.created_at)) <= ?", *date)
|
||||||
|
|
||||||
|
if ekspedisi {
|
||||||
|
query = query.Where("f.id IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
query = query.Where("f.id IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.
|
||||||
|
Order("COALESCE(e.realization_date, DATE(er.created_at)) ASC, er.id ASC").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockID(
|
||||||
|
ctx context.Context,
|
||||||
|
projectFlockID uint,
|
||||||
|
date *time.Time,
|
||||||
|
ekspedisi bool,
|
||||||
|
) ([]HppV2ExpenseCostRow, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return []HppV2ExpenseCostRow{}, nil
|
||||||
|
}
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]HppV2ExpenseCostRow, 0)
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Table("expense_realizations AS er").
|
||||||
|
Select(`
|
||||||
|
er.id AS expense_realization_id,
|
||||||
|
en.id AS expense_nonstock_id,
|
||||||
|
e.id AS expense_id,
|
||||||
|
COALESCE(n.id, 0) AS nonstock_id,
|
||||||
|
COALESCE(n.name, '') AS nonstock_name,
|
||||||
|
COALESCE(er.qty, 0) AS qty,
|
||||||
|
COALESCE(er.price, 0) AS price,
|
||||||
|
COALESCE(er.qty, 0) * COALESCE(er.price, 0) AS total_cost,
|
||||||
|
COALESCE(e.realization_date, DATE(er.created_at)) AS realization_date
|
||||||
|
`).
|
||||||
|
Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id").
|
||||||
|
Joins("JOIN expenses AS e ON e.id = en.expense_id").
|
||||||
|
Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id").
|
||||||
|
Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi).
|
||||||
|
Where("e.deleted_at IS NULL").
|
||||||
|
Where("e.category = ?", utils.ExpenseCategoryBOP).
|
||||||
|
Where("en.project_flock_kandang_id IS NULL").
|
||||||
|
Where("e.project_flock_id IS NOT NULL").
|
||||||
|
Where("e.project_flock_id::jsonb @> ?::jsonb", fmt.Sprintf("[%d]", projectFlockID)).
|
||||||
|
Where("COALESCE(e.realization_date, DATE(er.created_at)) <= ?", *date)
|
||||||
|
|
||||||
|
if ekspedisi {
|
||||||
|
query = query.Where("f.id IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
query = query.Where("f.id IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.
|
||||||
|
Order("COALESCE(e.realization_date, DATE(er.created_at)) ASC, er.id ASC").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HppV2RepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
|
func (r *HppV2RepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
|
||||||
if date == nil {
|
if date == nil {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -122,10 +438,13 @@ func (r *HppV2RepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKanda
|
|||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var adjustmentTotalWeight float64
|
var adjustmentTotals struct {
|
||||||
|
TotalQty float64
|
||||||
|
TotalWeight float64
|
||||||
|
}
|
||||||
adjustmentSubQuery := r.db.WithContext(ctx).
|
adjustmentSubQuery := r.db.WithContext(ctx).
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select("DISTINCT ast.id AS adjustment_id, ast.price AS price").
|
Select("DISTINCT ast.id AS adjustment_id, ast.total_qty AS total_qty, ast.price AS price").
|
||||||
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
|
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||||
Joins("JOIN stock_transfer_details AS std ON std.dest_product_warehouse_id = re.product_warehouse_id").
|
Joins("JOIN stock_transfer_details AS std ON std.dest_product_warehouse_id = re.product_warehouse_id").
|
||||||
Joins(
|
Joins(
|
||||||
@@ -141,13 +460,14 @@ func (r *HppV2RepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKanda
|
|||||||
|
|
||||||
err = r.db.WithContext(ctx).
|
err = r.db.WithContext(ctx).
|
||||||
Table("(?) AS adjustment_sources", adjustmentSubQuery).
|
Table("(?) AS adjustment_sources", adjustmentSubQuery).
|
||||||
Select("COALESCE(SUM(adjustment_sources.price), 0)").
|
Select("COALESCE(SUM(adjustment_sources.total_qty), 0) AS total_qty, COALESCE(SUM(adjustment_sources.price), 0) AS total_weight").
|
||||||
Scan(&adjustmentTotalWeight).Error
|
Scan(&adjustmentTotals).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
totals.TotalWeightKg += adjustmentTotalWeight
|
totals.TotalPieces += adjustmentTotals.TotalQty
|
||||||
|
totals.TotalWeightKg += adjustmentTotals.TotalWeight
|
||||||
|
|
||||||
return totals.TotalPieces, totals.TotalWeightKg, nil
|
return totals.TotalPieces, totals.TotalWeightKg, nil
|
||||||
}
|
}
|
||||||
@@ -200,7 +520,7 @@ sales_kandang AS (
|
|||||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
WHERE mdp.delivery_date IS NOT NULL
|
WHERE mdp.delivery_date IS NOT NULL
|
||||||
AND mdp.delivery_date <= ?
|
AND mdp.delivery_date <= ?
|
||||||
AND UPPER(COALESCE(w.type, '')) = 'KANDANG'
|
AND UPPER(COALESCE(w.type, '')) = 'KANDANG'
|
||||||
AND pw.project_flock_kandang_id IN (SELECT id FROM selected_pfk)
|
AND pw.project_flock_kandang_id IN (SELECT id FROM selected_pfk)
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
@@ -234,7 +554,7 @@ sales_lokasi AS (
|
|||||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
WHERE mdp.delivery_date IS NOT NULL
|
WHERE mdp.delivery_date IS NOT NULL
|
||||||
AND mdp.delivery_date <= ?
|
AND mdp.delivery_date <= ?
|
||||||
AND UPPER(COALESCE(w.type, '')) = 'LOKASI'
|
AND UPPER(COALESCE(w.type, '')) = 'LOKASI'
|
||||||
AND w.location_id IN (SELECT location_id FROM selected_locations)
|
AND w.location_id IN (SELECT location_id FROM selected_locations)
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
@@ -390,10 +710,10 @@ CROSS JOIN lokasi_rec_totals lrt
|
|||||||
Raw(
|
Raw(
|
||||||
query,
|
query,
|
||||||
projectFlockKandangIDs,
|
projectFlockKandangIDs,
|
||||||
*startDate,
|
*endDate,
|
||||||
entity.FlagableTypeProduct,
|
entity.FlagableTypeProduct,
|
||||||
eggFlags,
|
eggFlags,
|
||||||
*startDate,
|
*endDate,
|
||||||
entity.FlagableTypeProduct,
|
entity.FlagableTypeProduct,
|
||||||
eggFlags,
|
eggFlags,
|
||||||
string(utils.AdjustmentTransactionSubtypeRecordingEggIn),
|
string(utils.AdjustmentTransactionSubtypeRecordingEggIn),
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHppV2RepositoryGetEggProduksiIncludesTransferredAdjustmentStock(t *testing.T) {
|
||||||
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime) VALUES (1, 101, '2026-04-19 10:00:00')`,
|
||||||
|
`INSERT INTO recording_eggs (id, recording_id, product_warehouse_id, qty, weight, project_flock_kandang_id) VALUES (1, 1, 401, 80, 8, 101)`,
|
||||||
|
`INSERT INTO stock_transfers (id, transfer_date) VALUES (1, '2026-04-18 08:00:00')`,
|
||||||
|
`INSERT INTO stock_transfer_details (id, stock_transfer_id, source_product_warehouse_id, dest_product_warehouse_id) VALUES (1, 1, 301, 401)`,
|
||||||
|
`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose) VALUES (1, 'STOCK_TRANSFER_OUT', 1, 'ADJUSTMENT_IN', 501, 'ACTIVE', 'CONSUME')`,
|
||||||
|
`INSERT INTO adjustment_stocks (id, product_warehouse_id, total_qty, price, created_at) VALUES (501, 301, 20, 2.5, '2026-04-18 07:30:00')`,
|
||||||
|
)
|
||||||
|
|
||||||
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
|
endDate := mustJakartaTime(t, "2026-04-20 00:00:00")
|
||||||
|
|
||||||
|
totalPieces, totalWeightKg, err := repo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &endDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFloatEquals(t, totalPieces, 100)
|
||||||
|
assertFloatEquals(t, totalWeightKg, 10.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2RepositoryGetEggTerjualUsesEndDateForSameDayFarmSales(t *testing.T) {
|
||||||
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`INSERT INTO kandangs (id, location_id) VALUES (1, 10), (2, 10)`,
|
||||||
|
`INSERT INTO project_flock_kandangs (id, kandang_id) VALUES (101, 1), (102, 2)`,
|
||||||
|
`INSERT INTO warehouses (id, type, location_id) VALUES (201, 'LOKASI', 10)`,
|
||||||
|
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES (301, 201, 900, NULL)`,
|
||||||
|
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES (1, 'products', 900, 'TELUR')`,
|
||||||
|
`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES (1, 101, '2026-04-19 08:00:00', NULL), (2, 102, '2026-04-19 09:00:00', NULL)`,
|
||||||
|
`INSERT INTO recording_eggs (id, recording_id, product_warehouse_id, qty, weight, project_flock_kandang_id) VALUES (1, 1, 301, 60, 6, 101), (2, 2, 301, 40, 4, 102)`,
|
||||||
|
`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (401, 301)`,
|
||||||
|
`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty, total_weight, delivery_date) VALUES (501, 401, 50, 5, '2026-04-19 12:00:00')`,
|
||||||
|
)
|
||||||
|
|
||||||
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
|
startDate := mustJakartaTime(t, "2026-04-19 00:00:00")
|
||||||
|
endDate := mustJakartaTime(t, "2026-04-20 00:00:00")
|
||||||
|
|
||||||
|
totalPieces, totalWeightKg, err := repo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &startDate, &endDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFloatEquals(t, totalPieces, 30)
|
||||||
|
assertFloatEquals(t, totalWeightKg, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments(t *testing.T) {
|
||||||
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`INSERT INTO kandangs (id, location_id) VALUES (1, 10), (2, 10)`,
|
||||||
|
`INSERT INTO project_flock_kandangs (id, kandang_id) VALUES (101, 1), (102, 2)`,
|
||||||
|
`INSERT INTO warehouses (id, type, location_id) VALUES (201, 'LOKASI', 10), (211, 'KANDANG', 10), (212, 'KANDANG', 10)`,
|
||||||
|
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES (301, 201, 900, NULL), (311, 211, 900, 101), (312, 212, 900, 102)`,
|
||||||
|
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES (1, 'products', 900, 'TELUR')`,
|
||||||
|
`INSERT INTO stock_transfers (id, transfer_date) VALUES (1, '2026-04-18 08:00:00'), (2, '2026-04-18 08:15:00')`,
|
||||||
|
`INSERT INTO stock_transfer_details (id, stock_transfer_id, source_product_warehouse_id, dest_product_warehouse_id) VALUES (1, 1, 311, 301), (2, 2, 312, 301)`,
|
||||||
|
`INSERT INTO adjustment_stocks (id, product_warehouse_id, usage_qty, price, function_code, transaction_type, created_at) VALUES
|
||||||
|
(801, 311, 70, 7, 'RECORDING_EGG_IN', 'RECORDING', '2026-04-18 07:00:00'),
|
||||||
|
(802, 312, 30, 3, 'RECORDING_EGG_IN', 'RECORDING', '2026-04-18 07:30:00')`,
|
||||||
|
`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (401, 301)`,
|
||||||
|
`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty, total_weight, delivery_date) VALUES (501, 401, 20, 2, '2026-04-19 12:00:00')`,
|
||||||
|
)
|
||||||
|
|
||||||
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
|
startDate := mustJakartaTime(t, "2026-04-19 00:00:00")
|
||||||
|
endDate := mustJakartaTime(t, "2026-04-20 00:00:00")
|
||||||
|
|
||||||
|
totalPieces, totalWeightKg, err := repo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &startDate, &endDate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFloatEquals(t, totalPieces, 14)
|
||||||
|
assertFloatEquals(t, totalWeightKg, 1.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHppV2RepositoryTestDB(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
mustExecHppV2(t, db,
|
||||||
|
`CREATE TABLE recordings (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
project_flock_kandangs_id INTEGER NULL,
|
||||||
|
record_datetime DATETIME NULL,
|
||||||
|
deleted_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE recording_eggs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
recording_id INTEGER NULL,
|
||||||
|
product_warehouse_id INTEGER NULL,
|
||||||
|
qty NUMERIC(15,3) NULL,
|
||||||
|
weight NUMERIC(15,3) NULL,
|
||||||
|
project_flock_kandang_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE stock_transfers (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
transfer_date DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE stock_transfer_details (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
stock_transfer_id INTEGER NULL,
|
||||||
|
source_product_warehouse_id INTEGER NULL,
|
||||||
|
dest_product_warehouse_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE stock_allocations (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
usable_type TEXT NULL,
|
||||||
|
usable_id INTEGER NULL,
|
||||||
|
stockable_type TEXT NULL,
|
||||||
|
stockable_id INTEGER NULL,
|
||||||
|
status TEXT NULL,
|
||||||
|
allocation_purpose TEXT NULL,
|
||||||
|
qty NUMERIC(15,3) NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE adjustment_stocks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
product_warehouse_id INTEGER NULL,
|
||||||
|
total_qty NUMERIC(15,3) NULL,
|
||||||
|
usage_qty NUMERIC(15,3) NULL,
|
||||||
|
price NUMERIC(15,3) NULL,
|
||||||
|
grand_total NUMERIC(15,3) NULL,
|
||||||
|
function_code TEXT NULL,
|
||||||
|
transaction_type TEXT NULL,
|
||||||
|
created_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE kandangs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
location_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE project_flock_kandangs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
kandang_id INTEGER NULL,
|
||||||
|
project_flock_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE warehouses (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
type TEXT NULL,
|
||||||
|
location_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE product_warehouses (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
warehouse_id INTEGER NULL,
|
||||||
|
product_id INTEGER NULL,
|
||||||
|
project_flock_kandang_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE marketing_products (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
product_warehouse_id INTEGER NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE marketing_delivery_products (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
marketing_product_id INTEGER NULL,
|
||||||
|
usage_qty NUMERIC(15,3) NULL,
|
||||||
|
total_weight NUMERIC(15,3) NULL,
|
||||||
|
delivery_date DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE flags (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
flagable_type TEXT NULL,
|
||||||
|
flagable_id INTEGER NULL,
|
||||||
|
name TEXT NULL
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustExecHppV2(t *testing.T, db *gorm.DB, statements ...string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, statement := range statements {
|
||||||
|
if err := db.Exec(statement).Error; err != nil {
|
||||||
|
t.Fatalf("failed executing statement %q: %v", statement, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustJakartaTime(t *testing.T, raw string) time.Time {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed loading timezone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := time.ParseInLocation("2006-01-02 15:04:05", raw, location)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed parsing time %q: %v", raw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFloatEquals(t *testing.T, got float64, want float64) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if math.Abs(got-want) > 0.000001 {
|
||||||
|
t.Fatalf("expected %.6f, got %.6f", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2RepositoryConstantsStayAlignedWithProductionQueries(t *testing.T) {
|
||||||
|
if fifo.UsableKeyStockTransferOut.String() != "STOCK_TRANSFER_OUT" {
|
||||||
|
t.Fatalf("unexpected stock transfer usable key: %s", fifo.UsableKeyStockTransferOut.String())
|
||||||
|
}
|
||||||
|
if fifo.StockableKeyAdjustmentIn.String() != "ADJUSTMENT_IN" {
|
||||||
|
t.Fatalf("unexpected adjustment stockable key: %s", fifo.StockableKeyAdjustmentIn.String())
|
||||||
|
}
|
||||||
|
if entity.StockAllocationStatusActive != "ACTIVE" {
|
||||||
|
t.Fatalf("unexpected active stock allocation status: %s", entity.StockAllocationStatusActive)
|
||||||
|
}
|
||||||
|
if entity.StockAllocationPurposeConsume != "CONSUME" {
|
||||||
|
t.Fatalf("unexpected consume stock allocation purpose: %s", entity.StockAllocationPurposeConsume)
|
||||||
|
}
|
||||||
|
if string(utils.AdjustmentTransactionSubtypeRecordingEggIn) != "RECORDING_EGG_IN" {
|
||||||
|
t.Fatalf("unexpected adjustment function code: %s", utils.AdjustmentTransactionSubtypeRecordingEggIn)
|
||||||
|
}
|
||||||
|
if string(utils.AdjustmentTransactionTypeRecording) != "RECORDING" {
|
||||||
|
t.Fatalf("unexpected adjustment transaction type: %s", utils.AdjustmentTransactionTypeRecording)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type HppV2DateWindow struct {
|
||||||
|
Start string `json:"start"`
|
||||||
|
End string `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2Proration struct {
|
||||||
|
Basis string `json:"basis"`
|
||||||
|
Numerator float64 `json:"numerator"`
|
||||||
|
Denominator float64 `json:"denominator"`
|
||||||
|
Ratio float64 `json:"ratio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2Reference struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID uint `json:"id"`
|
||||||
|
StockableType string `json:"stockable_type,omitempty"`
|
||||||
|
ProjectFlockKandangID *uint `json:"project_flock_kandang_id,omitempty"`
|
||||||
|
ProductID uint `json:"product_id,omitempty"`
|
||||||
|
ProductName string `json:"product_name,omitempty"`
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
AppliedTotal float64 `json:"applied_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2ComponentPart struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Proration *HppV2Proration `json:"proration,omitempty"`
|
||||||
|
References []HppV2Reference `json:"references,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2Component struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Parts []HppV2ComponentPart `json:"parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppV2Breakdown struct {
|
||||||
|
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||||
|
ProjectFlockID uint `json:"project_flock_id"`
|
||||||
|
ProjectFlockCategory string `json:"project_flock_category,omitempty"`
|
||||||
|
KandangID uint `json:"kandang_id,omitempty"`
|
||||||
|
KandangName string `json:"kandang_name,omitempty"`
|
||||||
|
LocationID uint `json:"location_id,omitempty"`
|
||||||
|
PeriodDate string `json:"period_date"`
|
||||||
|
Window HppV2DateWindow `json:"window"`
|
||||||
|
TotalProductionCost float64 `json:"total_production_cost"`
|
||||||
|
Components []HppV2Component `json:"components"`
|
||||||
|
Hpp HppCostResponse `json:"hpp"`
|
||||||
|
}
|
||||||
@@ -5,13 +5,40 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hppV2ComponentPakan = "PAKAN"
|
||||||
|
hppV2ComponentOvk = "OVK"
|
||||||
|
hppV2ComponentBopRegular = "BOP_REGULAR"
|
||||||
|
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
||||||
|
hppV2PartGrowingNormal = "growing_normal"
|
||||||
|
hppV2PartGrowingCutover = "growing_cutover"
|
||||||
|
hppV2PartLayingNormal = "laying_normal"
|
||||||
|
hppV2PartLayingCutover = "laying_cutover"
|
||||||
|
hppV2PartGrowingDirect = "growing_direct"
|
||||||
|
hppV2PartGrowingFarm = "growing_farm"
|
||||||
|
hppV2PartLayingDirect = "laying_direct"
|
||||||
|
hppV2PartLayingFarm = "laying_farm"
|
||||||
|
hppV2ProrationPopulation = "growing_population_share"
|
||||||
|
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
||||||
|
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
||||||
|
hppV2CutoverFlagPakan = "PAKAN-CUTOVER"
|
||||||
|
hppV2CutoverFlagOvk = "OVK-CUTOVER"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HppV2Service interface {
|
type HppV2Service interface {
|
||||||
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
|
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
|
||||||
|
CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error)
|
||||||
GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
GetFeedGrowing(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
GetFeedLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
|
GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error)
|
||||||
|
GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
|
GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
|
GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
|
GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error)
|
||||||
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
|
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,101 +46,676 @@ type hppV2Service struct {
|
|||||||
hppRepo commonRepo.HppV2CostRepository
|
hppRepo commonRepo.HppV2CostRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hppV2StockComponentConfig struct {
|
||||||
|
Code string
|
||||||
|
Title string
|
||||||
|
NormalFlags []string
|
||||||
|
CutoverFlags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type hppV2ExpenseComponentConfig struct {
|
||||||
|
Code string
|
||||||
|
Title string
|
||||||
|
Ekspedisi bool
|
||||||
|
}
|
||||||
|
|
||||||
func NewHppV2Service(hppRepo commonRepo.HppV2CostRepository) HppV2Service {
|
func NewHppV2Service(hppRepo commonRepo.HppV2CostRepository) HppV2Service {
|
||||||
return &hppV2Service{hppRepo: hppRepo}
|
return &hppV2Service{hppRepo: hppRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
|
func (s *hppV2Service) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
|
||||||
if date == nil {
|
breakdown, err := s.CalculateHppBreakdown(projectFlockKandangId, date)
|
||||||
now := time.Now()
|
if err != nil {
|
||||||
date = &now
|
return nil, err
|
||||||
|
}
|
||||||
|
if breakdown == nil {
|
||||||
|
return &HppCostResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
result := breakdown.Hpp
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error) {
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return &HppV2Breakdown{
|
||||||
|
ProjectFlockKandangID: projectFlockKandangId,
|
||||||
|
Hpp: HppCostResponse{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
startOfDay, endOfDay, err := hppV2DayWindow(date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
|
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
|
||||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
|
||||||
|
|
||||||
pakan, err := s.GetCostPakan(projectFlockKandangId, &endOfDay)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.GetHppEstimationDanRealisasi(pakan, projectFlockKandangId, &startOfDay, &endOfDay)
|
pakanComponent, err := s.GetPakanBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
totalProductionCost := 0.0
|
||||||
|
components := make([]HppV2Component, 0, 4)
|
||||||
|
if pakanComponent != nil && (pakanComponent.Total > 0 || len(pakanComponent.Parts) > 0) {
|
||||||
|
totalProductionCost += pakanComponent.Total
|
||||||
|
components = append(components, *pakanComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ovkComponent != nil && (ovkComponent.Total > 0 || len(ovkComponent.Parts) > 0) {
|
||||||
|
totalProductionCost += ovkComponent.Total
|
||||||
|
components = append(components, *ovkComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bopRegularComponent != nil && (bopRegularComponent.Total > 0 || len(bopRegularComponent.Parts) > 0) {
|
||||||
|
totalProductionCost += bopRegularComponent.Total
|
||||||
|
components = append(components, *bopRegularComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bopEkspedisiComponent != nil && (bopEkspedisiComponent.Total > 0 || len(bopEkspedisiComponent.Parts) > 0) {
|
||||||
|
totalProductionCost += bopEkspedisiComponent.Total
|
||||||
|
components = append(components, *bopEkspedisiComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hppCost == nil {
|
||||||
|
hppCost = &HppCostResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Breakdown{
|
||||||
|
ProjectFlockKandangID: projectFlockKandangId,
|
||||||
|
ProjectFlockID: contextRow.ProjectFlockID,
|
||||||
|
ProjectFlockCategory: contextRow.ProjectFlockCategory,
|
||||||
|
KandangID: contextRow.KandangID,
|
||||||
|
KandangName: contextRow.KandangName,
|
||||||
|
LocationID: contextRow.LocationID,
|
||||||
|
PeriodDate: startOfDay.Format("2006-01-02"),
|
||||||
|
Window: HppV2DateWindow{
|
||||||
|
Start: startOfDay.Format(time.RFC3339),
|
||||||
|
End: endOfDay.Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
TotalProductionCost: totalProductionCost,
|
||||||
|
Components: components,
|
||||||
|
Hpp: *hppCost,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
func (s *hppV2Service) GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
||||||
feedGrowing, err := s.GetFeedGrowing(projectFlockKandangId, endDate)
|
component, err := s.GetPakanBreakdown(projectFlockKandangId, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if component == nil {
|
||||||
feedLaying, err := s.GetFeedLaying(projectFlockKandangId, endDate)
|
return 0, nil
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pakan := feedGrowing + feedLaying
|
return component.Total, nil
|
||||||
return pakan, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) GetFeedGrowing(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
func (s *hppV2Service) GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
|
||||||
if s.hppRepo == nil {
|
return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{
|
||||||
|
Code: hppV2ComponentPakan,
|
||||||
|
Title: "Pakan",
|
||||||
|
NormalFlags: []string{string(utils.FlagPakan)},
|
||||||
|
CutoverFlags: []string{hppV2CutoverFlagPakan},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
||||||
|
component, err := s.GetOvkBreakdown(projectFlockKandangId, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return component.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
|
||||||
|
return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{
|
||||||
|
Code: hppV2ComponentOvk,
|
||||||
|
Title: "OVK",
|
||||||
|
NormalFlags: []string{
|
||||||
|
string(utils.FlagOVK),
|
||||||
|
string(utils.FlagObat),
|
||||||
|
string(utils.FlagVitamin),
|
||||||
|
string(utils.FlagKimia),
|
||||||
|
},
|
||||||
|
CutoverFlags: []string{hppV2CutoverFlagOvk},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
||||||
|
component, err := s.GetBopRegularBreakdown(projectFlockKandangId, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return component.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
||||||
|
component, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return component.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
|
||||||
|
return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{
|
||||||
|
Code: hppV2ComponentBopRegular,
|
||||||
|
Title: "BOP Regular",
|
||||||
|
Ekspedisi: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) {
|
||||||
|
return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{
|
||||||
|
Code: hppV2ComponentBopEksp,
|
||||||
|
Title: "BOP Ekspedisi",
|
||||||
|
Ekspedisi: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig) (*HppV2Component, error) {
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: config.Code,
|
||||||
|
Title: config.Title,
|
||||||
|
Parts: []HppV2ComponentPart{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]HppV2ComponentPart, 0, 4)
|
||||||
|
total := 0.0
|
||||||
|
|
||||||
|
growingPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if growingPart != nil {
|
||||||
|
parts = append(parts, *growingPart)
|
||||||
|
total += growingPart.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
growingCutoverPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if growingCutoverPart != nil {
|
||||||
|
parts = append(parts, *growingCutoverPart)
|
||||||
|
total += growingCutoverPart.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
layingNormalPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if layingNormalPart != nil {
|
||||||
|
parts = append(parts, *layingNormalPart)
|
||||||
|
total += layingNormalPart.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if layingCutoverPart != nil {
|
||||||
|
parts = append(parts, *layingCutoverPart)
|
||||||
|
total += layingCutoverPart.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: config.Code,
|
||||||
|
Title: config.Title,
|
||||||
|
Total: total,
|
||||||
|
Parts: parts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig) (*HppV2Component, error) {
|
||||||
|
if s.hppRepo == nil {
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: config.Code,
|
||||||
|
Title: config.Title,
|
||||||
|
Parts: []HppV2ComponentPart{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]HppV2ComponentPart, 0, 4)
|
||||||
|
total := 0.0
|
||||||
|
|
||||||
|
growingDirect, err := s.buildGrowingExpenseDirectPart(projectFlockKandangId, contextRow, endDate, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if growingDirect != nil {
|
||||||
|
parts = append(parts, *growingDirect)
|
||||||
|
total += growingDirect.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
growingFarm, err := s.buildGrowingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if growingFarm != nil {
|
||||||
|
parts = append(parts, *growingFarm)
|
||||||
|
total += growingFarm.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
layingDirect, err := s.buildLayingExpenseDirectPart(projectFlockKandangId, endDate, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if layingDirect != nil {
|
||||||
|
parts = append(parts, *layingDirect)
|
||||||
|
total += layingDirect.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
layingFarm, err := s.buildLayingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if layingFarm != nil {
|
||||||
|
parts = append(parts, *layingFarm)
|
||||||
|
total += layingFarm.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2Component{
|
||||||
|
Code: config.Code,
|
||||||
|
Title: config.Title,
|
||||||
|
Total: total,
|
||||||
|
Parts: parts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildGrowingUsagePart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2StockComponentConfig,
|
||||||
|
cutover bool,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if contextRow == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
|
if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
|
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(kandangIDsGrowing) == 0 {
|
if len(kandangIDsGrowing) == 0 {
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
|
||||||
|
|
||||||
feedUsageCostGrowing, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDsGrowing, endDate)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
|
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if totalPopulationFlockGrowing == 0 {
|
if totalPopulationFlockGrowing == 0 {
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := feedUsageCostGrowing * (transferTotalQty / totalPopulationFlockGrowing)
|
ratio := transferTotalQty / totalPopulationFlockGrowing
|
||||||
return result, nil
|
if ratio <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
partCode := hppV2PartGrowingNormal
|
||||||
|
partTitle := "Growing"
|
||||||
|
baseRows := make([]HppV2Reference, 0)
|
||||||
|
baseTotal := 0.0
|
||||||
|
|
||||||
|
if cutover {
|
||||||
|
partCode = hppV2PartGrowingCutover
|
||||||
|
partTitle = "Growing Cut-over"
|
||||||
|
|
||||||
|
rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.CutoverFlags, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
rowTotal := adjustmentRowTotalCost(row)
|
||||||
|
baseTotal += rowTotal
|
||||||
|
baseRows = append(baseRows, HppV2Reference{
|
||||||
|
Type: "adjustment_stock",
|
||||||
|
ID: row.AdjustmentID,
|
||||||
|
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Date: row.CreatedAt.Format("2006-01-02"),
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.Price,
|
||||||
|
Total: rowTotal,
|
||||||
|
AppliedTotal: rowTotal * ratio,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.NormalFlags, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
baseTotal += row.TotalCost
|
||||||
|
refDate := row.LastUsedAt
|
||||||
|
if refDate.IsZero() {
|
||||||
|
refDate = row.FirstUsedAt
|
||||||
|
}
|
||||||
|
baseRows = append(baseRows, HppV2Reference{
|
||||||
|
Type: "stock_allocation",
|
||||||
|
ID: row.StockableID,
|
||||||
|
StockableType: row.StockableType,
|
||||||
|
ProductID: row.SourceProductID,
|
||||||
|
ProductName: row.SourceProductName,
|
||||||
|
Date: refDate.Format("2006-01-02"),
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.UnitPrice,
|
||||||
|
Total: row.TotalCost,
|
||||||
|
AppliedTotal: row.TotalCost * ratio,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseTotal == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: partCode,
|
||||||
|
Title: partTitle,
|
||||||
|
Total: baseTotal * ratio,
|
||||||
|
Proration: &HppV2Proration{
|
||||||
|
Basis: hppV2ProrationPopulation,
|
||||||
|
Numerator: transferTotalQty,
|
||||||
|
Denominator: totalPopulationFlockGrowing,
|
||||||
|
Ratio: ratio,
|
||||||
|
},
|
||||||
|
References: baseRows,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) GetFeedLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
|
func (s *hppV2Service) buildLayingUsagePart(
|
||||||
if s.hppRepo == nil {
|
projectFlockKandangId uint,
|
||||||
return 0, nil
|
endDate *time.Time,
|
||||||
|
config hppV2StockComponentConfig,
|
||||||
|
cutover bool,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if cutover {
|
||||||
|
rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.CutoverFlags, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
references := make([]HppV2Reference, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
rowTotal := adjustmentRowTotalCost(row)
|
||||||
|
total += rowTotal
|
||||||
|
references = append(references, HppV2Reference{
|
||||||
|
Type: "adjustment_stock",
|
||||||
|
ID: row.AdjustmentID,
|
||||||
|
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Date: row.CreatedAt.Format("2006-01-02"),
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.Price,
|
||||||
|
Total: rowTotal,
|
||||||
|
AppliedTotal: rowTotal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: hppV2PartLayingCutover,
|
||||||
|
Title: "Laying Cut-over",
|
||||||
|
Total: total,
|
||||||
|
References: references,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
|
rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.NormalFlags, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
total := 0.0
|
||||||
|
references := make([]HppV2Reference, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
total += row.TotalCost
|
||||||
|
refDate := row.LastUsedAt
|
||||||
|
if refDate.IsZero() {
|
||||||
|
refDate = row.FirstUsedAt
|
||||||
|
}
|
||||||
|
references = append(references, HppV2Reference{
|
||||||
|
Type: "stock_allocation",
|
||||||
|
ID: row.StockableID,
|
||||||
|
StockableType: row.StockableType,
|
||||||
|
ProductID: row.SourceProductID,
|
||||||
|
ProductName: row.SourceProductName,
|
||||||
|
Date: refDate.Format("2006-01-02"),
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.UnitPrice,
|
||||||
|
Total: row.TotalCost,
|
||||||
|
AppliedTotal: row.TotalCost,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: hppV2PartLayingNormal,
|
||||||
|
Title: "Laying",
|
||||||
|
Total: total,
|
||||||
|
References: references,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildGrowingExpenseDirectPart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2ExpenseComponentConfig,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildGrowingExpenseFarmPart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2ExpenseComponentConfig,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildGrowingExpensePart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2ExpenseComponentConfig,
|
||||||
|
farmLevel bool,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if contextRow == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(kandangIDsGrowing) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if totalPopulationFlockGrowing <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := transferTotalQty / totalPopulationFlockGrowing
|
||||||
|
if ratio <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []commonRepo.HppV2ExpenseCostRow
|
||||||
|
if farmLevel {
|
||||||
|
rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), sourceProjectFlockID, endDate, config.Ekspedisi)
|
||||||
|
} else {
|
||||||
|
rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), kandangIDsGrowing, endDate, config.Ekspedisi)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildExpensePartFromRows(
|
||||||
|
rows,
|
||||||
|
map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel],
|
||||||
|
map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel],
|
||||||
|
&HppV2Proration{
|
||||||
|
Basis: hppV2ProrationPopulation,
|
||||||
|
Numerator: transferTotalQty,
|
||||||
|
Denominator: totalPopulationFlockGrowing,
|
||||||
|
Ratio: ratio,
|
||||||
|
},
|
||||||
|
ratio,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildLayingExpenseDirectPart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2ExpenseComponentConfig,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), []uint{projectFlockKandangId}, endDate, config.Ekspedisi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", nil, 1), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2Service) buildLayingExpenseFarmPart(
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||||
|
endDate *time.Time,
|
||||||
|
config hppV2ExpenseComponentConfig,
|
||||||
|
) (*HppV2ComponentPart, error) {
|
||||||
|
if contextRow == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), contextRow.ProjectFlockID, endDate, config.Ekspedisi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
basis := hppV2ProrationEggWeight
|
||||||
|
numerator := targetWeight
|
||||||
|
denominator := farmWeight
|
||||||
|
if denominator <= 0 {
|
||||||
|
basis = hppV2ProrationEggPiece
|
||||||
|
numerator = targetPieces
|
||||||
|
denominator = farmPieces
|
||||||
|
}
|
||||||
|
if denominator <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := numerator / denominator
|
||||||
|
if ratio <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildExpensePartFromRows(
|
||||||
|
rows,
|
||||||
|
hppV2PartLayingFarm,
|
||||||
|
"Laying Farm",
|
||||||
|
&HppV2Proration{
|
||||||
|
Basis: basis,
|
||||||
|
Numerator: numerator,
|
||||||
|
Denominator: denominator,
|
||||||
|
Ratio: ratio,
|
||||||
|
},
|
||||||
|
ratio,
|
||||||
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
|
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
|
||||||
@@ -155,9 +757,71 @@ func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64,
|
|||||||
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
|
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &HppCostResponse{
|
return &HppCostResponse{
|
||||||
Estimation: estimation,
|
Estimation: estimation,
|
||||||
Real: real,
|
Real: real,
|
||||||
}
|
}, nil
|
||||||
return result, nil
|
}
|
||||||
|
|
||||||
|
func hppV2DayWindow(date *time.Time) (time.Time, time.Time, error) {
|
||||||
|
if date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
date = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
|
||||||
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||||
|
return startOfDay, endOfDay, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjustmentRowTotalCost(row commonRepo.HppV2AdjustmentCostRow) float64 {
|
||||||
|
if row.GrandTotal > 0 {
|
||||||
|
return row.GrandTotal
|
||||||
|
}
|
||||||
|
return row.Qty * row.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildExpensePartFromRows(
|
||||||
|
rows []commonRepo.HppV2ExpenseCostRow,
|
||||||
|
code string,
|
||||||
|
title string,
|
||||||
|
proration *HppV2Proration,
|
||||||
|
ratio float64,
|
||||||
|
) *HppV2ComponentPart {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
references := make([]HppV2Reference, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
total += row.TotalCost * ratio
|
||||||
|
references = append(references, HppV2Reference{
|
||||||
|
Type: "expense_realization",
|
||||||
|
ID: row.ExpenseRealizationID,
|
||||||
|
ProductID: row.NonstockID,
|
||||||
|
ProductName: row.NonstockName,
|
||||||
|
Date: row.RealizationDate.Format("2006-01-02"),
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.Price,
|
||||||
|
Total: row.TotalCost,
|
||||||
|
AppliedTotal: row.TotalCost * ratio,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HppV2ComponentPart{
|
||||||
|
Code: code,
|
||||||
|
Title: title,
|
||||||
|
Total: total,
|
||||||
|
Proration: proration,
|
||||||
|
References: references,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hppV2RepoStub struct {
|
||||||
|
contextByPFK map[uint]*commonRepo.HppV2ProjectFlockKandangContext
|
||||||
|
pfkIDsByProject map[uint][]uint
|
||||||
|
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
|
||||||
|
adjustRowsByKey map[string][]commonRepo.HppV2AdjustmentCostRow
|
||||||
|
expenseRowsByPFKKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
|
expenseRowsByFarmKey map[string][]commonRepo.HppV2ExpenseCostRow
|
||||||
|
totalPopulationByKey map[string]float64
|
||||||
|
transferSummaryByPFK map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}
|
||||||
|
eggProductionByPFK map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}
|
||||||
|
eggSalesByPFK map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetProjectFlockKandangContext(_ context.Context, projectFlockKandangId uint) (*commonRepo.HppV2ProjectFlockKandangContext, error) {
|
||||||
|
row := s.contextByPFK[projectFlockKandangId]
|
||||||
|
if row == nil {
|
||||||
|
return nil, fmt.Errorf("pfk %d not found", projectFlockKandangId)
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetProjectFlockKandangIDs(_ context.Context, projectFlockId uint) ([]uint, error) {
|
||||||
|
return append([]uint{}, s.pfkIDsByProject[projectFlockId]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListAdjustmentCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2AdjustmentCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2AdjustmentCostRow{}, s.adjustRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockKandangIDs(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByPFKKey[expenseStubKey(projectFlockKandangIDs, ekspedisi)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) ListExpenseRealizationRowsByProjectFlockID(_ context.Context, projectFlockID uint, _ *time.Time, ekspedisi bool) ([]commonRepo.HppV2ExpenseCostRow, error) {
|
||||||
|
return append([]commonRepo.HppV2ExpenseCostRow{}, s.expenseRowsByFarmKey[expenseFarmKey(projectFlockID, ekspedisi)]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetFeedUsageCost(_ context.Context, _ []uint, _ *time.Time) (float64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetTotalPopulation(_ context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||||
|
return s.totalPopulationByKey[stubKey(projectFlockKandangIDs, nil)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time) (float64, float64, error) {
|
||||||
|
totalPieces := 0.0
|
||||||
|
totalKg := 0.0
|
||||||
|
for _, projectFlockKandangID := range projectFlockKandangIDs {
|
||||||
|
row := s.eggProductionByPFK[projectFlockKandangID]
|
||||||
|
totalPieces += row.pieces
|
||||||
|
totalKg += row.kg
|
||||||
|
}
|
||||||
|
return totalPieces, totalKg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(_ context.Context, projectFlockKandangIDs []uint, _ *time.Time, _ *time.Time) (float64, float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) != 1 {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
row := s.eggSalesByPFK[projectFlockKandangIDs[0]]
|
||||||
|
return row.pieces, row.kg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hppV2RepoStub) GetTransferSourceSummary(_ context.Context, projectFlockKandangId uint) (uint, float64, error) {
|
||||||
|
row := s.transferSummaryByPFK[projectFlockKandangId]
|
||||||
|
return row.projectFlockID, row.totalQty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
10: {
|
||||||
|
ProjectFlockKandangID: 10,
|
||||||
|
ProjectFlockID: 2,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 100,
|
||||||
|
KandangName: "Kandang A",
|
||||||
|
LocationID: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
1: {101, 102},
|
||||||
|
},
|
||||||
|
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||||
|
stubKey([]uint{101, 102}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9001, SourceProductID: 8, SourceProductName: "Pakan Growing", Qty: 100, UnitPrice: 40, TotalCost: 4000},
|
||||||
|
},
|
||||||
|
stubKey([]uint{10}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9002, SourceProductID: 9, SourceProductName: "Pakan Laying", Qty: 50, UnitPrice: 30, TotalCost: 1500},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
|
||||||
|
stubKey([]uint{101, 102}, []string{"PAKAN-CUTOVER"}): {
|
||||||
|
{AdjustmentID: 8001, ProductID: 11, ProductName: "Pakan Growing Cut-over", Qty: 20, Price: 30, GrandTotal: 600},
|
||||||
|
},
|
||||||
|
stubKey([]uint{10}, []string{"PAKAN-CUTOVER"}): {
|
||||||
|
{AdjustmentID: 8002, ProductID: 12, ProductName: "Pakan Laying Cut-over", Qty: 10, Price: 30, GrandTotal: 300},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{101, 102}, nil): 1000,
|
||||||
|
},
|
||||||
|
transferSummaryByPFK: map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}{
|
||||||
|
10: {projectFlockID: 1, totalQty: 250},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
10: {pieces: 100, kg: 10},
|
||||||
|
},
|
||||||
|
eggSalesByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
10: {pieces: 40, kg: 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(10, mustDate(t, "2026-04-19"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected breakdown result")
|
||||||
|
}
|
||||||
|
if got := result.TotalProductionCost; got != 2950 {
|
||||||
|
t.Fatalf("expected total production cost 2950, got %v", got)
|
||||||
|
}
|
||||||
|
if len(result.Components) != 1 {
|
||||||
|
t.Fatalf("expected 1 component, got %d", len(result.Components))
|
||||||
|
}
|
||||||
|
component := result.Components[0]
|
||||||
|
if component.Code != "PAKAN" {
|
||||||
|
t.Fatalf("expected PAKAN component, got %s", component.Code)
|
||||||
|
}
|
||||||
|
partTotals := map[string]float64{}
|
||||||
|
for _, part := range component.Parts {
|
||||||
|
partTotals[part.Code] = part.Total
|
||||||
|
}
|
||||||
|
if partTotals[hppV2PartGrowingNormal] != 1000 {
|
||||||
|
t.Fatalf("expected growing normal 1000, got %v", partTotals[hppV2PartGrowingNormal])
|
||||||
|
}
|
||||||
|
if partTotals[hppV2PartGrowingCutover] != 150 {
|
||||||
|
t.Fatalf("expected growing cutover 150, got %v", partTotals[hppV2PartGrowingCutover])
|
||||||
|
}
|
||||||
|
if partTotals[hppV2PartLayingNormal] != 1500 {
|
||||||
|
t.Fatalf("expected laying normal 1500, got %v", partTotals[hppV2PartLayingNormal])
|
||||||
|
}
|
||||||
|
if partTotals[hppV2PartLayingCutover] != 300 {
|
||||||
|
t.Fatalf("expected laying cutover 300, got %v", partTotals[hppV2PartLayingCutover])
|
||||||
|
}
|
||||||
|
if component.Parts[0].Proration == nil || component.Parts[0].Proration.Ratio != 0.25 {
|
||||||
|
t.Fatalf("expected growing proration ratio 0.25, got %+v", component.Parts[0].Proration)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 295 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 295, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
if result.Hpp.Real.HargaKg != 737.5 {
|
||||||
|
t.Fatalf("expected real harga/kg 737.5, got %v", result.Hpp.Real.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_ManualCutoverUsesLayingSlicesOnly(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
20: {
|
||||||
|
ProjectFlockKandangID: 20,
|
||||||
|
ProjectFlockID: 3,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 200,
|
||||||
|
KandangName: "Kandang B",
|
||||||
|
LocationID: 17,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||||
|
stubKey([]uint{20}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9100, SourceProductID: 21, SourceProductName: "Pakan Laying", Qty: 20, UnitPrice: 10, TotalCost: 200},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
|
||||||
|
stubKey([]uint{20}, []string{"PAKAN-CUTOVER"}): {
|
||||||
|
{AdjustmentID: 8100, ProductID: 22, ProductName: "Pakan Laying Cut-over", Qty: 30, Price: 10, GrandTotal: 300},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
20: {pieces: 50, kg: 5},
|
||||||
|
},
|
||||||
|
eggSalesByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
20: {pieces: 25, kg: 2.5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(20, mustDate(t, "2026-04-19"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 500 {
|
||||||
|
t.Fatalf("expected total production cost 500, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
component := result.Components[0]
|
||||||
|
if len(component.Parts) != 2 {
|
||||||
|
t.Fatalf("expected 2 laying parts, got %d", len(component.Parts))
|
||||||
|
}
|
||||||
|
for _, part := range component.Parts {
|
||||||
|
if strings.HasPrefix(part.Code, "growing_") {
|
||||||
|
t.Fatalf("expected no growing parts, got %s", part.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 100 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 100, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
30: {
|
||||||
|
ProjectFlockKandangID: 30,
|
||||||
|
ProjectFlockID: 4,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 300,
|
||||||
|
KandangName: "Kandang C",
|
||||||
|
LocationID: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
5: {301, 302},
|
||||||
|
},
|
||||||
|
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||||
|
stubKey([]uint{30}, []string{"PAKAN"}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9200, SourceProductID: 31, SourceProductName: "Pakan Laying", Qty: 20, UnitPrice: 25, TotalCost: 500},
|
||||||
|
},
|
||||||
|
stubKey([]uint{301, 302}, []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9201, SourceProductID: 32, SourceProductName: "OVK Growing", Qty: 40, UnitPrice: 10, TotalCost: 400},
|
||||||
|
},
|
||||||
|
stubKey([]uint{30}, []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}): {
|
||||||
|
{StockableType: "purchase_items", StockableID: 9202, SourceProductID: 33, SourceProductName: "OVK Laying", Qty: 15, UnitPrice: 10, TotalCost: 150},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{
|
||||||
|
stubKey([]uint{301, 302}, []string{"OVK-CUTOVER"}): {
|
||||||
|
{AdjustmentID: 8201, ProductID: 34, ProductName: "OVK Growing Cut-over", Qty: 10, Price: 10, GrandTotal: 100},
|
||||||
|
},
|
||||||
|
stubKey([]uint{30}, []string{"OVK-CUTOVER"}): {
|
||||||
|
{AdjustmentID: 8202, ProductID: 35, ProductName: "OVK Laying Cut-over", Qty: 5, Price: 10, GrandTotal: 50},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{301, 302}, nil): 1000,
|
||||||
|
},
|
||||||
|
transferSummaryByPFK: map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}{
|
||||||
|
30: {projectFlockID: 5, totalQty: 500},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
30: {pieces: 120, kg: 12},
|
||||||
|
},
|
||||||
|
eggSalesByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
30: {pieces: 60, kg: 6},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(30, mustDate(t, "2026-04-19"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected breakdown result")
|
||||||
|
}
|
||||||
|
if len(result.Components) != 2 {
|
||||||
|
t.Fatalf("expected 2 components, got %d", len(result.Components))
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTotals := map[string]float64{}
|
||||||
|
for _, component := range result.Components {
|
||||||
|
componentTotals[component.Code] = component.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
if componentTotals[hppV2ComponentPakan] != 500 {
|
||||||
|
t.Fatalf("expected pakan total 500, got %v", componentTotals[hppV2ComponentPakan])
|
||||||
|
}
|
||||||
|
if componentTotals[hppV2ComponentOvk] != 450 {
|
||||||
|
t.Fatalf("expected ovk total 450, got %v", componentTotals[hppV2ComponentOvk])
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 950 {
|
||||||
|
t.Fatalf("expected total production cost 950, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 79.17 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 79.17, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T) {
|
||||||
|
repo := &hppV2RepoStub{
|
||||||
|
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||||
|
40: {
|
||||||
|
ProjectFlockKandangID: 40,
|
||||||
|
ProjectFlockID: 6,
|
||||||
|
ProjectFlockCategory: "LAYING",
|
||||||
|
KandangID: 400,
|
||||||
|
KandangName: "Kandang D",
|
||||||
|
LocationID: 19,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pfkIDsByProject: map[uint][]uint{
|
||||||
|
6: {40, 41},
|
||||||
|
7: {701, 702},
|
||||||
|
},
|
||||||
|
totalPopulationByKey: map[string]float64{
|
||||||
|
stubKey([]uint{701, 702}, nil): 1000,
|
||||||
|
},
|
||||||
|
transferSummaryByPFK: map[uint]struct {
|
||||||
|
projectFlockID uint
|
||||||
|
totalQty float64
|
||||||
|
}{
|
||||||
|
40: {projectFlockID: 7, totalQty: 200},
|
||||||
|
},
|
||||||
|
expenseRowsByPFKKey: map[string][]commonRepo.HppV2ExpenseCostRow{
|
||||||
|
expenseStubKey([]uint{701, 702}, false): {
|
||||||
|
{ExpenseRealizationID: 1, NonstockID: 11, NonstockName: "Growing BOP", Qty: 1, Price: 500, TotalCost: 500, RealizationDate: mustTime(t, "2026-04-10")},
|
||||||
|
},
|
||||||
|
expenseStubKey([]uint{40}, false): {
|
||||||
|
{ExpenseRealizationID: 2, NonstockID: 12, NonstockName: "Laying BOP", Qty: 1, Price: 80, TotalCost: 80, RealizationDate: mustTime(t, "2026-04-19")},
|
||||||
|
},
|
||||||
|
expenseStubKey([]uint{701, 702}, true): {
|
||||||
|
{ExpenseRealizationID: 3, NonstockID: 13, NonstockName: "Growing Expedition", Qty: 1, Price: 100, TotalCost: 100, RealizationDate: mustTime(t, "2026-04-11")},
|
||||||
|
},
|
||||||
|
expenseStubKey([]uint{40}, true): {
|
||||||
|
{ExpenseRealizationID: 4, NonstockID: 14, NonstockName: "Laying Expedition", Qty: 1, Price: 40, TotalCost: 40, RealizationDate: mustTime(t, "2026-04-19")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expenseRowsByFarmKey: map[string][]commonRepo.HppV2ExpenseCostRow{
|
||||||
|
expenseFarmKey(7, false): {
|
||||||
|
{ExpenseRealizationID: 5, NonstockID: 15, NonstockName: "Growing Farm BOP", Qty: 1, Price: 300, TotalCost: 300, RealizationDate: mustTime(t, "2026-04-12")},
|
||||||
|
},
|
||||||
|
expenseFarmKey(6, false): {
|
||||||
|
{ExpenseRealizationID: 6, NonstockID: 16, NonstockName: "Laying Farm BOP", Qty: 1, Price: 100, TotalCost: 100, RealizationDate: mustTime(t, "2026-04-19")},
|
||||||
|
},
|
||||||
|
expenseFarmKey(7, true): {
|
||||||
|
{ExpenseRealizationID: 7, NonstockID: 17, NonstockName: "Growing Farm Expedition", Qty: 1, Price: 50, TotalCost: 50, RealizationDate: mustTime(t, "2026-04-12")},
|
||||||
|
},
|
||||||
|
expenseFarmKey(6, true): {
|
||||||
|
{ExpenseRealizationID: 8, NonstockID: 18, NonstockName: "Laying Farm Expedition", Qty: 1, Price: 60, TotalCost: 60, RealizationDate: mustTime(t, "2026-04-19")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eggProductionByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
40: {pieces: 30, kg: 3},
|
||||||
|
41: {pieces: 70, kg: 7},
|
||||||
|
},
|
||||||
|
eggSalesByPFK: map[uint]struct {
|
||||||
|
pieces float64
|
||||||
|
kg float64
|
||||||
|
}{
|
||||||
|
40: {pieces: 50, kg: 5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewHppV2Service(repo)
|
||||||
|
result, err := svc.CalculateHppBreakdown(40, mustDate(t, "2026-04-19"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTotals := map[string]float64{}
|
||||||
|
for _, component := range result.Components {
|
||||||
|
componentTotals[component.Code] = component.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
if componentTotals[hppV2ComponentBopRegular] != 270 {
|
||||||
|
t.Fatalf("expected regular BOP total 270, got %v", componentTotals[hppV2ComponentBopRegular])
|
||||||
|
}
|
||||||
|
if componentTotals[hppV2ComponentBopEksp] != 88 {
|
||||||
|
t.Fatalf("expected expedition BOP total 88, got %v", componentTotals[hppV2ComponentBopEksp])
|
||||||
|
}
|
||||||
|
if result.TotalProductionCost != 358 {
|
||||||
|
t.Fatalf("expected total production cost 358, got %v", result.TotalProductionCost)
|
||||||
|
}
|
||||||
|
if result.Hpp.Estimation.HargaKg != 119.33 {
|
||||||
|
t.Fatalf("expected estimation harga/kg 119.33, got %v", result.Hpp.Estimation.HargaKg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stubKey(ids []uint, flags []string) string {
|
||||||
|
idParts := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
idParts = append(idParts, fmt.Sprintf("%d", id))
|
||||||
|
}
|
||||||
|
sort.Strings(idParts)
|
||||||
|
|
||||||
|
flagParts := append([]string{}, flags...)
|
||||||
|
sort.Strings(flagParts)
|
||||||
|
|
||||||
|
return strings.Join(idParts, ",") + "|" + strings.Join(flagParts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDate(t *testing.T, raw string) *time.Time {
|
||||||
|
t.Helper()
|
||||||
|
loc, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load timezone: %v", err)
|
||||||
|
}
|
||||||
|
value, err := time.ParseInLocation("2006-01-02", raw, loc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse date %s: %v", raw, err)
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustTime(t *testing.T, raw string) time.Time {
|
||||||
|
t.Helper()
|
||||||
|
value := mustDate(t, raw)
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
|
func expenseStubKey(ids []uint, ekspedisi bool) string {
|
||||||
|
return stubKey(ids, []string{fmt.Sprintf("ekspedisi=%t", ekspedisi)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func expenseFarmKey(projectFlockID uint, ekspedisi bool) string {
|
||||||
|
return fmt.Sprintf("farm=%d|ekspedisi=%t", projectFlockID, ekspedisi)
|
||||||
|
}
|
||||||
@@ -498,6 +498,29 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *RepportController) GetHppV2Breakdown(ctx *fiber.Ctx) error {
|
||||||
|
query := &validation.HppV2BreakdownQuery{
|
||||||
|
ProjectFlockKandangID: uint(ctx.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
Period: ctx.Query("period", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.EnsureProjectFlockKandangAccess(ctx, c.RepportService.DB(), 0, query.ProjectFlockKandangID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.RepportService.GetHppV2Breakdown(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get HPP v2 breakdown successfully",
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
|
func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
|
||||||
return parseCommaSeparatedInt64sWithField(raw, "supplier_ids")
|
return parseCommaSeparatedInt64sWithField(raw, "supplier_ids")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
|
|||||||
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
|
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
|
||||||
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
|
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
|
||||||
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang)
|
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang)
|
||||||
|
route.Get("/hpp-v2-breakdown", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppV2Breakdown)
|
||||||
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
|
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
|
||||||
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
|
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type RepportService interface {
|
|||||||
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
|
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
|
||||||
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
|
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
|
||||||
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
|
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
|
||||||
|
GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error)
|
||||||
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
|
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
|
||||||
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
|
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
@@ -2146,6 +2147,27 @@ func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Locatio
|
|||||||
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
|
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if s.HppV2Svc == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "hpp v2 service is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
periodDate, err := time.ParseInLocation("2006-01-02", params.Period, time.FixedZone("Asia/Jakarta", 7*60*60))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.HppV2Svc.CalculateHppBreakdown(params.ProjectFlockKandangID, &periodDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ type HppPerKandangQuery struct {
|
|||||||
WeightMax *float64 `query:"-"`
|
WeightMax *float64 `query:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HppV2BreakdownQuery struct {
|
||||||
|
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"required,gt=0"`
|
||||||
|
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExpenseDepreciationQuery struct {
|
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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user