Files
lti-api/internal/common/repository/common.hppv2.repository.go
T
2026-04-22 12:57:41 +07:00

1192 lines
39 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"time"
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"
)
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 HppV2ChickinCostRow struct {
ProjectChickinID uint
ProjectFlockKandangID uint
ChickInDate time.Time
StockableType string
StockableID uint
SourceProductID uint
SourceProductName string
Qty float64
UnitPrice float64
TotalCost float64
}
type HppV2LatestTransferInputRow struct {
ProjectFlockKandangID uint
SourceProjectFlockID uint
TransferDate time.Time
TransferQty float64
TransferID uint
}
type HppV2ManualDepreciationInputRow struct {
ID uint
ProjectFlockID uint
TotalCost float64
CutoverDate time.Time
Note *string
}
type HppV2FarmDepreciationSnapshotRow struct {
ID uint
ProjectFlockID uint
PeriodDate time.Time
DepreciationPercentEffective float64
DepreciationValue float64
PulletCostDayNTotal float64
}
type HppV2CostRepository interface {
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error)
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error)
GetRecordingStockRoutingAdjustmentCostByProjectFlockID(ctx context.Context, projectFlockID uint, periodDate time.Time) (float64, error)
GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error)
GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
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)
ListChickinCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, excludeTransferToLaying bool) ([]HppV2ChickinCostRow, error)
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
}
type HppV2RepositoryImpl struct {
db *gorm.DB
}
func NewHppV2CostRepository(db *gorm.DB) HppV2CostRepository {
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) {
var ids []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("id").
Where("project_flock_id = ?", projectFlockId).
Scan(&ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
func (r *HppV2RepositoryImpl) GetLatestTransferInputByProjectFlockKandangID(
ctx context.Context,
projectFlockKandangId uint,
period time.Time,
) (*HppV2LatestTransferInputRow, error) {
var row HppV2LatestTransferInputRow
query := `
WITH latest_transfer_approval AS (
SELECT a.approvable_id, a.action
FROM approvals a
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = @approval_type
GROUP BY approvable_id
) la
ON la.approvable_id = a.approvable_id
AND la.latest_action_at = a.action_at
WHERE a.approvable_type = @approval_type
),
approved_transfers AS (
SELECT
lt.id,
lt.from_project_flock_id,
COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date
FROM laying_transfers lt
JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id
WHERE lt.deleted_at IS NULL
AND lt.executed_at IS NOT NULL
AND lta.action = 'APPROVED'
)
SELECT
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
at.from_project_flock_id AS source_project_flock_id,
at.effective_date AS transfer_date,
ltt.total_qty AS transfer_qty,
at.id AS transfer_id
FROM laying_transfer_targets ltt
JOIN approved_transfers at ON at.id = ltt.laying_transfer_id
WHERE ltt.deleted_at IS NULL
AND ltt.target_project_flock_kandang_id = @project_flock_kandang_id
AND at.effective_date <= DATE(@period_date)
ORDER BY at.effective_date DESC, at.id DESC
LIMIT 1
`
err := r.db.WithContext(ctx).Raw(query, map[string]any{
"approval_type": utils.ApprovalWorkflowTransferToLaying.String(),
"project_flock_kandang_id": projectFlockKandangId,
"period_date": period,
}).Scan(&row).Error
if err != nil {
return nil, err
}
if row.TransferID == 0 {
return nil, nil
}
return &row, nil
}
func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
ctx context.Context,
projectFlockID uint,
) (*HppV2ManualDepreciationInputRow, error) {
var row HppV2ManualDepreciationInputRow
err := r.db.WithContext(ctx).
Table("farm_depreciation_manual_inputs").
Select("id, project_flock_id, total_cost, cutover_date, note").
Where("project_flock_id = ?", projectFlockID).
Limit(1).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlockID(
ctx context.Context,
projectFlockID uint,
periodDate time.Time,
) (float64, error) {
if projectFlockID == 0 || periodDate.IsZero() {
return 0, nil
}
flags := []utils.FlagType{
utils.FlagPakan,
utils.FlagOVK,
utils.FlagObat,
utils.FlagVitamin,
utils.FlagKimia,
}
transferExistsCondition := `
EXISTS (
SELECT 1
FROM laying_transfer_targets AS ltt
JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id
WHERE ltt.deleted_at IS NULL
AND lt.deleted_at IS NULL
AND lt.executed_at IS NOT NULL
AND ltt.target_project_flock_kandang_id = r.project_flock_kandangs_id
AND COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) <= DATE(?)
AND (
SELECT a.action
FROM approvals a
WHERE a.approvable_type = ?
AND a.approvable_id = lt.id
ORDER BY a.id DESC
LIMIT 1
) = ?
)
`
var total float64
err := r.db.WithContext(ctx).
Table("recording_stocks AS rs").
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)").
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
Joins("JOIN project_flock_kandangs AS pfk_rec ON pfk_rec.id = r.project_flock_kandangs_id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
fifo.UsableKeyRecordingStock.String(),
fifo.StockableKeyPurchaseItems.String(),
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("pfk_rec.project_flock_id = ?", projectFlockID).
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
Where(
fmt.Sprintf(
"((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)",
transferExistsCondition,
transferExistsCondition,
),
periodDate,
string(utils.ApprovalWorkflowTransferToLaying),
entity.ApprovalActionApproved,
periodDate,
string(utils.ApprovalWorkflowTransferToLaying),
entity.ApprovalActionApproved,
).
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(
ctx context.Context,
projectFlockID uint,
periodDate time.Time,
) (*HppV2FarmDepreciationSnapshotRow, error) {
var row HppV2FarmDepreciationSnapshotRow
err := r.db.WithContext(ctx).
Table("farm_depreciation_snapshots").
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total").
Where("project_flock_id = ?", projectFlockID).
Where("period_date = DATE(?)", periodDate).
Limit(1).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) {
type row struct {
ChickInDate *time.Time
}
var selected row
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("MIN(pc.chick_in_date) AS chick_in_date").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Where("pc.deleted_at IS NULL").
Where("pfk.project_flock_id = ?", projectFlockID).
Scan(&selected).Error
if err != nil {
return nil, err
}
if selected.ChickInDate == nil || selected.ChickInDate.IsZero() {
return nil, nil
}
return selected.ChickInDate, nil
}
func (r *HppV2RepositoryImpl) GetDepreciationPercents(
ctx context.Context,
houseTypes []string,
maxDay int,
) (map[string]map[int]float64, error) {
result := make(map[string]map[int]float64)
if len(houseTypes) == 0 || maxDay <= 0 {
return result, nil
}
type row struct {
HouseType string
Day int
DepreciationPercent float64
}
rows := make([]row, 0)
err := r.db.WithContext(ctx).
Table("house_depreciation_standards").
Select("house_type::text AS house_type, day, depreciation_percent").
Where("house_type::text IN ?", houseTypes).
Where("day <= ?", maxDay).
Order("house_type ASC, day ASC").
Scan(&rows).Error
if err != nil {
return nil, err
}
for _, item := range rows {
if _, exists := result[item.HouseType]; !exists {
result[item.HouseType] = make(map[int]float64)
}
result[item.HouseType][item.Day] = item.DepreciationPercent
}
return result, nil
}
func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
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,
COALESCE(MAX(CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0
END), 0) 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("rs.project_flock_kandang_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, '')
`).
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) ListChickinCostRowsByProductFlags(
ctx context.Context,
projectFlockKandangIDs []uint,
flagNames []string,
date *time.Time,
excludeTransferToLaying bool,
) ([]HppV2ChickinCostRow, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return []HppV2ChickinCostRow{}, nil
}
if date == nil {
now := time.Now()
date = &now
}
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
usableStockTransferOut := fifo.UsableKeyStockTransferOut.String()
unitPriceExpr := fmt.Sprintf(`
CASE
WHEN sa.stockable_type = '%s' THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(ast.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(spi.price, sast.price, 0)
WHEN sa.stockable_type = '%s' THEN COALESCE(tpi.price, tast.price, 0)
ELSE 0
END
`, stockablePurchase, stockableAdjustment, stockableTransferIn, stockableTransferToLaying)
rows := make([]HppV2ChickinCostRow, 0)
query := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
pc.id AS project_chickin_id,
pc.project_flock_kandang_id AS project_flock_kandang_id,
pc.chick_in_date AS chick_in_date,
sa.stockable_type AS stockable_type,
sa.stockable_id AS stockable_id,
COALESCE(
pi.product_id,
ast_pw.product_id,
tpi.product_id,
tast_pw.product_id,
spi.product_id,
sast_pw.product_id,
0
) AS source_product_id,
COALESCE(
pi_prod.name,
ast_prod.name,
tpi_prod.name,
tast_prod.name,
spi_prod.name,
sast_prod.name,
''
) AS source_product_name,
COALESCE(SUM(sa.qty), 0) AS qty,
`+unitPriceExpr+` AS unit_price,
COALESCE(SUM(sa.qty * (`+unitPriceExpr+`)), 0) AS total_cost
`).
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?",
usableProjectChickin,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeTraceChickin,
).
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").
Joins(
"LEFT JOIN stock_allocations AS tsa_transfer ON tsa_transfer.usable_type = ? AND tsa_transfer.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_transfer.status = ? AND tsa_transfer.allocation_purpose = ?",
stockableTransferToLaying,
stockableTransferToLaying,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN products AS tpi_prod ON tpi_prod.id = tpi.product_id").
Joins("LEFT JOIN adjustment_stocks AS tast ON tast.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockableAdjustment).
Joins("LEFT JOIN product_warehouses AS tast_pw ON tast_pw.id = tast.product_warehouse_id").
Joins("LEFT JOIN products AS tast_prod ON tast_prod.id = tast_pw.product_id").
Joins(
"LEFT JOIN stock_allocations AS tsa_stock ON tsa_stock.usable_type = ? AND tsa_stock.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_stock.status = ? AND tsa_stock.allocation_purpose = ?",
usableStockTransferOut,
stockableTransferIn,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("LEFT JOIN purchase_items AS spi ON spi.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN products AS spi_prod ON spi_prod.id = spi.product_id").
Joins("LEFT JOIN adjustment_stocks AS sast ON sast.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockableAdjustment).
Joins("LEFT JOIN product_warehouses AS sast_pw ON sast_pw.id = sast.product_warehouse_id").
Joins("LEFT JOIN products AS sast_prod ON sast_prod.id = sast_pw.product_id").
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("pc.chick_in_date <= ?", *date).
Where(`
EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_type = ?
AND f.flagable_id = COALESCE(
pi.product_id,
ast_pw.product_id,
tpi.product_id,
tast_pw.product_id,
spi.product_id,
sast_pw.product_id,
0
)
AND f.name IN ?
)
`, entity.FlagableTypeProduct, flagNames)
if excludeTransferToLaying {
query = query.Where("sa.stockable_type <> ?", stockableTransferToLaying)
}
err := query.
Group(fmt.Sprintf(`
pc.id,
pc.project_flock_kandang_id,
pc.chick_in_date,
sa.stockable_type,
sa.stockable_id,
COALESCE(
pi.product_id,
ast_pw.product_id,
tpi.product_id,
tast_pw.product_id,
spi.product_id,
sast_pw.product_id,
0
),
COALESCE(
pi_prod.name,
ast_prod.name,
tpi_prod.name,
tast_prod.name,
spi_prod.name,
sast_prod.name,
''
),
%s
`, unitPriceExpr)).
Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_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) {
if date == nil {
now := time.Now()
date = &now
}
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
usableRecordingStock := fifo.UsableKeyRecordingStock.String()
var total float64
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
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)`,
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 flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
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 adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
Where("rs.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Where("f.name = ?", utils.FlagPakan).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppV2RepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty), 0)").
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppV2RepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
if date == nil {
now := time.Now()
date = &now
}
var totals struct {
TotalPieces float64
TotalWeightKg float64
}
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg").
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
var adjustmentTotals struct {
TotalQty float64
TotalWeight float64
}
adjustmentSubQuery := r.db.WithContext(ctx).
Table("recordings AS r").
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 stock_transfer_details AS std ON std.dest_product_warehouse_id = re.product_warehouse_id").
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = std.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
fifo.UsableKeyStockTransferOut.String(),
fifo.StockableKeyAdjustmentIn.String(),
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND ast.product_warehouse_id = std.source_product_warehouse_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date)
err = r.db.WithContext(ctx).
Table("(?) AS adjustment_sources", adjustmentSubQuery).
Select("COALESCE(SUM(adjustment_sources.total_qty), 0) AS total_qty, COALESCE(SUM(adjustment_sources.price), 0) AS total_weight").
Scan(&adjustmentTotals).Error
if err != nil {
return 0, 0, err
}
totals.TotalPieces += adjustmentTotals.TotalQty
totals.TotalWeightKg += adjustmentTotals.TotalWeight
return totals.TotalPieces, totals.TotalWeightKg, nil
}
func (r *HppV2RepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(
ctx context.Context,
projectFlockKandangIDs []uint,
startDate *time.Time,
endDate *time.Time,
) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
if endDate == nil {
now := time.Now()
endDate = &now
}
if startDate == nil {
startDate = endDate
}
eggFlags := []string{
string(utils.FlagTelur),
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
string(utils.FlagTelurPapacal),
string(utils.FlagTelurJumbo),
}
query := `
WITH selected_pfk AS (
SELECT pfk.id, k.location_id
FROM project_flock_kandangs pfk
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE pfk.id IN ?
),
selected_locations AS (
SELECT DISTINCT location_id
FROM selected_pfk
),
sales_kandang AS (
SELECT DISTINCT
mdp.id AS mdp_id,
COALESCE(mdp.usage_qty, 0) AS usage_qty,
COALESCE(mdp.total_weight, 0) AS total_weight
FROM marketing_delivery_products mdp
JOIN marketing_products mp ON mp.id = mdp.marketing_product_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE mdp.delivery_date IS NOT NULL
AND mdp.delivery_date <= ?
AND UPPER(COALESCE(w.type, '')) = 'KANDANG'
AND pw.project_flock_kandang_id IN (SELECT id FROM selected_pfk)
AND EXISTS (
SELECT 1
FROM recording_eggs re
JOIN recordings rr ON rr.id = re.recording_id
WHERE re.product_warehouse_id = mp.product_warehouse_id
AND COALESCE(re.project_flock_kandang_id, rr.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk)
AND rr.deleted_at IS NULL
AND DATE(rr.record_datetime) <= DATE(mdp.delivery_date)
)
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_type = ?
AND f.flagable_id = pw.product_id
AND f.name IN ?
)
),
sales_lokasi AS (
SELECT DISTINCT
mdp.id AS mdp_id,
COALESCE(mdp.usage_qty, 0) AS usage_qty,
COALESCE(mdp.total_weight, 0) AS total_weight,
mdp.delivery_date AS delivery_date,
pw.id AS lokasi_pw_id,
pw.product_id AS product_id,
w.location_id AS location_id
FROM marketing_delivery_products mdp
JOIN marketing_products mp ON mp.id = mdp.marketing_product_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE mdp.delivery_date IS NOT NULL
AND mdp.delivery_date <= ?
AND UPPER(COALESCE(w.type, '')) = 'LOKASI'
AND w.location_id IN (SELECT location_id FROM selected_locations)
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_type = ?
AND f.flagable_id = pw.product_id
AND f.name IN ?
)
),
transfer_pairs AS (
SELECT
std.source_product_warehouse_id AS source_pw_id,
std.dest_product_warehouse_id AS dest_pw_id,
MIN(st.transfer_date) AS first_transfer_date
FROM stock_transfer_details std
JOIN stock_transfers st ON st.id = std.stock_transfer_id
WHERE std.source_product_warehouse_id IS NOT NULL
AND std.dest_product_warehouse_id IS NOT NULL
GROUP BY std.source_product_warehouse_id, std.dest_product_warehouse_id
),
adj_pool AS (
SELECT
sl.mdp_id,
SUM(CASE
WHEN spw.project_flock_kandang_id IN (SELECT id FROM selected_pfk)
THEN COALESCE(ast.usage_qty, 0)
ELSE 0
END) AS sel_usage_qty,
SUM(COALESCE(ast.usage_qty, 0)) AS farm_usage_qty,
SUM(CASE
WHEN spw.project_flock_kandang_id IN (SELECT id FROM selected_pfk)
THEN COALESCE(ast.price, 0)
ELSE 0
END) AS sel_price_sum,
SUM(COALESCE(ast.price, 0)) AS farm_price_sum
FROM sales_lokasi sl
JOIN transfer_pairs tf
ON tf.dest_pw_id = sl.lokasi_pw_id
AND DATE(tf.first_transfer_date) <= DATE(sl.delivery_date)
JOIN product_warehouses spw
ON spw.id = tf.source_pw_id
AND spw.product_id = sl.product_id
JOIN warehouses sw ON sw.id = spw.warehouse_id
JOIN adjustment_stocks ast ON ast.product_warehouse_id = tf.source_pw_id
WHERE UPPER(COALESCE(sw.type, '')) = 'KANDANG'
AND sw.location_id = sl.location_id
AND UPPER(COALESCE(ast.function_code, '')) = UPPER(?)
AND UPPER(COALESCE(ast.transaction_type, '')) = UPPER(?)
AND DATE(ast.created_at) <= DATE(sl.delivery_date)
GROUP BY sl.mdp_id
),
sales_lokasi_adj AS (
SELECT sl.*
FROM sales_lokasi sl
JOIN adj_pool ap ON ap.mdp_id = sl.mdp_id
WHERE COALESCE(ap.farm_usage_qty, 0) > 0
OR COALESCE(ap.farm_price_sum, 0) > 0
),
sales_lokasi_rec AS (
SELECT sl.*
FROM sales_lokasi sl
WHERE NOT EXISTS (
SELECT 1 FROM sales_lokasi_adj sla WHERE sla.mdp_id = sl.mdp_id
)
),
rec_pool AS (
SELECT
sl.mdp_id,
SUM(CASE
WHEN COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk)
THEN COALESCE(re.qty, 0)
ELSE 0
END) AS sel_qty,
SUM(COALESCE(re.qty, 0)) AS farm_qty,
SUM(CASE
WHEN COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk)
THEN COALESCE(re.weight, 0)
ELSE 0
END) AS sel_weight,
SUM(COALESCE(re.weight, 0)) AS farm_weight
FROM sales_lokasi_rec sl
JOIN recordings r
ON r.deleted_at IS NULL
AND DATE(r.record_datetime) <= DATE(sl.delivery_date)
JOIN recording_eggs re
ON re.recording_id = r.id
AND re.product_warehouse_id = sl.lokasi_pw_id
JOIN project_flock_kandangs pfk
ON pfk.id = COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id)
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE k.location_id = sl.location_id
GROUP BY sl.mdp_id
),
kandang_totals AS (
SELECT
COALESCE(SUM(sk.usage_qty), 0) AS total_pieces,
COALESCE(SUM(sk.total_weight), 0) AS total_weight
FROM sales_kandang sk
),
lokasi_adj_totals AS (
SELECT
COALESCE(SUM(
sla.usage_qty *
CASE
WHEN COALESCE(ap.farm_usage_qty, 0) > 0 THEN (COALESCE(ap.sel_usage_qty, 0) * 1.0) / NULLIF(ap.farm_usage_qty, 0)
ELSE 0
END
), 0) AS total_pieces,
COALESCE(SUM(
sla.total_weight *
CASE
WHEN COALESCE(ap.farm_price_sum, 0) > 0 THEN (COALESCE(ap.sel_price_sum, 0) * 1.0) / NULLIF(ap.farm_price_sum, 0)
ELSE 0
END
), 0) AS total_weight
FROM sales_lokasi_adj sla
JOIN adj_pool ap ON ap.mdp_id = sla.mdp_id
),
lokasi_rec_totals AS (
SELECT
COALESCE(SUM(
slr.usage_qty *
CASE
WHEN COALESCE(rp.farm_qty, 0) > 0 THEN (COALESCE(rp.sel_qty, 0) * 1.0) / NULLIF(rp.farm_qty, 0)
ELSE 0
END
), 0) AS total_pieces,
COALESCE(SUM(
slr.total_weight *
CASE
WHEN COALESCE(rp.farm_weight, 0) > 0 THEN (COALESCE(rp.sel_weight, 0) * 1.0) / NULLIF(rp.farm_weight, 0)
ELSE 0
END
), 0) AS total_weight
FROM sales_lokasi_rec slr
LEFT JOIN rec_pool rp ON rp.mdp_id = slr.mdp_id
)
SELECT
COALESCE(kt.total_pieces, 0) + COALESCE(lat.total_pieces, 0) + COALESCE(lrt.total_pieces, 0) AS total_pieces,
COALESCE(kt.total_weight, 0) + COALESCE(lat.total_weight, 0) + COALESCE(lrt.total_weight, 0) AS total_weight
FROM kandang_totals kt
CROSS JOIN lokasi_adj_totals lat
CROSS JOIN lokasi_rec_totals lrt
`
var totals struct {
TotalPieces float64
TotalWeight float64
}
err := r.db.WithContext(ctx).
Raw(
query,
projectFlockKandangIDs,
*endDate,
entity.FlagableTypeProduct,
eggFlags,
*endDate,
entity.FlagableTypeProduct,
eggFlags,
string(utils.AdjustmentTransactionSubtypeRecordingEggIn),
string(utils.AdjustmentTransactionTypeRecording),
).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
return totals.TotalPieces, totals.TotalWeight, nil
}
func (r *HppV2RepositoryImpl) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) {
var summary struct {
ProjectFlockID uint
TotalQty float64
}
err := r.db.WithContext(ctx).
Table("laying_transfer_targets AS ltt").
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
Where("lt.deleted_at IS NULL").
Where("ltt.deleted_at IS NULL").
Where("lt.executed_at IS NOT NULL").
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
Group("lt.from_project_flock_id").
Scan(&summary).Error
if err != nil {
return 0, 0, err
}
return summary.ProjectFlockID, summary.TotalQty, nil
}