Files
lti-api/internal/modules/closings/repositories/closingKeuangan.repository.go
T

774 lines
27 KiB
Go

package repository
import (
"context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
// ClosingKeuanganRepository handles database operations for closing keuangan
type ClosingKeuanganRepository interface {
repository.BaseRepository[interface{}]
// Egg Production
GetTotalEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalQty int, totalWeightKg float64, err error)
GetTotalEggProductionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (totalQty int, totalWeightKg float64, totalRecordings int, err error)
GetEggProductionByProjectFlockKandangIDsWithDetails(ctx context.Context, projectFlockKandangIDs []uint) ([]EggProductionDetailRow, error)
GetCumulativeEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalQty int, totalWeightKg float64, err error)
// Population Data
GetTotalPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (totalPopulation float64, err error)
GetTotalPopulationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalPopulation float64, err error)
GetRemainingPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (remainingPopulation float64, err error)
// Budget Data
GetTotalBudgetByProjectFlockID(ctx context.Context, projectFlockID uint) (totalBudget float64, err error)
GetTotalBudgetByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalBudget float64, err error)
// Realization/Expense Data
GetTotalRealizationByProjectFlockID(ctx context.Context, projectFlockID uint) (totalRealization float64, err error)
GetTotalRealizationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalRealization float64, err error)
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
// Expedition
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
// Products
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
// All Product Usage
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]ProductUsageRow, error)
}
type ClosingKeuanganRepositoryImpl struct {
*repository.BaseRepositoryImpl[interface{}]
}
func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository {
return &ClosingKeuanganRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db),
}
}
// Result Rows
type EggProductionDetailRow struct {
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
KandangName string `gorm:"column:kandang_name"`
TotalQty int `gorm:"column:total_qty"`
TotalWeightKg float64 `gorm:"column:total_weight_kg"`
TotalRecordings int `gorm:"column:total_recordings"`
}
type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"`
}
type ActualUsageCostRow struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
FlagName string `gorm:"column:flag_name"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
AveragePrice float64 `gorm:"column:average_price"`
}
type ProductUsageRow struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
FlagNames string `gorm:"column:flag_names"`
TotalQty float64 `gorm:"column:total_qty"`
Price float64 `gorm:"column:price"`
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
}
// === EGG PRODUCTION QUERIES ===
// GetTotalEggProductionByProjectFlockID gets total egg production for all kandangs in a project flock
func (r *ClosingKeuanganRepositoryImpl) GetTotalEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (int, float64, error) {
if projectFlockID == 0 {
return 0, 0, nil
}
var result struct {
TotalQty float64
TotalWeightKg float64
}
err := r.DB().WithContext(ctx).
Table("project_flocks pf").
Select(`
COALESCE(SUM(re.qty), 0) AS total_qty,
COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg
`).
Joins("JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id").
Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id").
Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id").
Where("pf.id = ?", projectFlockID).
Where("pf.deleted_at IS NULL").
Where("r.deleted_at IS NULL").
Scan(&result).Error
if err != nil {
return 0, 0, err
}
return int(result.TotalQty), result.TotalWeightKg, nil
}
// GetTotalEggProductionByProjectFlockKandangID gets total egg production for a specific kandang
func (r *ClosingKeuanganRepositoryImpl) GetTotalEggProductionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int, float64, int, error) {
if projectFlockKandangID == 0 {
return 0, 0, 0, nil
}
var result struct {
TotalQty float64
TotalWeightKg float64
TotalRecordings int
}
err := r.DB().WithContext(ctx).
Table("project_flock_kandangs pfk").
Select(`
COALESCE(SUM(re.qty), 0) AS total_qty,
COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg,
COUNT(DISTINCT r.id) AS total_recordings
`).
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id").
Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id").
Where("pfk.id = ?", projectFlockKandangID).
Where("pf.deleted_at IS NULL").
Where("r.deleted_at IS NULL").
Scan(&result).Error
if err != nil {
return 0, 0, 0, err
}
return int(result.TotalQty), result.TotalWeightKg, result.TotalRecordings, nil
}
// GetEggProductionByProjectFlockKandangIDsWithDetails gets egg production details for multiple kandangs
func (r *ClosingKeuanganRepositoryImpl) GetEggProductionByProjectFlockKandangIDsWithDetails(ctx context.Context, projectFlockKandangIDs []uint) ([]EggProductionDetailRow, error) {
if len(projectFlockKandangIDs) == 0 {
return []EggProductionDetailRow{}, nil
}
var results []EggProductionDetailRow
err := r.DB().WithContext(ctx).
Table("project_flock_kandangs pfk").
Select(`
pfk.id AS project_flock_kandang_id,
k.name AS kandang_name,
COALESCE(SUM(re.qty), 0) AS total_qty,
COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg,
COUNT(DISTINCT r.id) AS total_recordings
`).
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Joins("JOIN kandangs k ON k.id = pfk.kandang_id").
Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id").
Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id").
Where("pfk.id IN ?", projectFlockKandangIDs).
Where("pf.deleted_at IS NULL").
Where("r.deleted_at IS NULL").
Group("pfk.id, k.name").
Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// GetCumulativeEggProductionByProjectFlockID gets cumulative egg production for project flock
func (r *ClosingKeuanganRepositoryImpl) GetCumulativeEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (int, float64, error) {
if projectFlockID == 0 {
return 0, 0, nil
}
var result struct {
TotalQty float64
TotalWeightKg float64
}
err := r.DB().WithContext(ctx).
Table("project_flocks pf").
Select(`
COALESCE(SUM(re.qty), 0) AS total_qty,
COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg
`).
Joins("JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN recordings r ON r.project_flock_kandangs_id = pfk.id").
Joins("JOIN recording_eggs re ON re.recording_id = r.id").
Where("pf.id = ?", projectFlockID).
Where("pf.deleted_at IS NULL").
Where("r.deleted_at IS NULL").
Where("r.record_datetime <= (SELECT MAX(record_datetime) FROM recordings WHERE project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID).
Scan(&result).Error
if err != nil {
return 0, 0, err
}
return int(result.TotalQty), result.TotalWeightKg, nil
}
// === POPULATION QUERIES ===
// GetTotalPopulationByProjectFlockID gets total initial population for project flock
func (r *ClosingKeuanganRepositoryImpl) GetTotalPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("project_chickins").
Select("COALESCE(SUM(qty), 0)").
Where("project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
}
// GetTotalPopulationByProjectFlockKandangIDs gets total population for multiple kandangs
func (r *ClosingKeuanganRepositoryImpl) GetTotalPopulationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("project_chickins").
Select("COALESCE(SUM(qty), 0)").
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Scan(&result).Error
return result, err
}
// GetRemainingPopulationByProjectFlockID gets remaining population based on depletion
func (r *ClosingKeuanganRepositoryImpl) GetRemainingPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result struct {
TotalChickin float64
TotalDepletion float64
}
err := r.DB().WithContext(ctx).
Table("project_flocks pf").
Select(`
COALESCE((SELECT SUM(qty) FROM project_chickins WHERE project_flock_id = pf.id), 0) AS total_chickin,
COALESCE((SELECT SUM(COALESCE(rd.qty, 0))
FROM recordings r
JOIN recording_depletions rd ON rd.recording_id = r.id
JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id
WHERE pfk.project_flock_id = pf.id), 0) AS total_depletion
`).
Where("pf.id = ?", projectFlockID).
Scan(&result).Error
if err != nil {
return 0, err
}
return result.TotalChickin - result.TotalDepletion, nil
}
// === BUDGET QUERIES ===
// GetTotalBudgetByProjectFlockID gets total budget for project flock
func (r *ClosingKeuanganRepositoryImpl) GetTotalBudgetByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("project_budgets pb").
Select("COALESCE(SUM(pb.amount), 0)").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = pb.project_flock_kandang_id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("pb.deleted_at IS NULL").
Scan(&result).Error
return result, err
}
// GetTotalBudgetByProjectFlockKandangIDs gets total budget for multiple kandangs
func (r *ClosingKeuanganRepositoryImpl) GetTotalBudgetByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("project_budgets pb").
Select("COALESCE(SUM(pb.amount), 0)").
Where("pb.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("pb.deleted_at IS NULL").
Scan(&result).Error
return result, err
}
// === REALIZATION/EXPENSE QUERIES ===
// GetTotalRealizationByProjectFlockID gets total expense realization for project flock
func (r *ClosingKeuanganRepositoryImpl) GetTotalRealizationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("expense_realizations er").
Select("COALESCE(SUM(er.amount), 0)").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = er.project_flock_kandang_id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("er.deleted_at IS NULL").
Scan(&result).Error
return result, err
}
// GetTotalRealizationByProjectFlockKandangIDs gets total realization for multiple kandangs
func (r *ClosingKeuanganRepositoryImpl) GetTotalRealizationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var result float64
err := r.DB().WithContext(ctx).
Table("expense_realizations er").
Select("COALESCE(SUM(er.amount), 0)").
Where("er.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("er.deleted_at IS NULL").
Scan(&result).Error
return result, err
}
// GetActualUsageCostByProjectFlockID gets actual usage cost by project flock
func (r *ClosingKeuanganRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
if projectFlockID == 0 {
return []ActualUsageCostRow{}, nil
}
db := r.DB().WithContext(ctx)
// Get all project flock kandang IDs for this project flock
var pfkIDs []uint
err := db.Table("project_flock_kandangs").
Where("project_flock_id = ?", projectFlockID).
Pluck("id", &pfkIDs).Error
if err != nil {
return nil, err
}
if len(pfkIDs) == 0 {
return []ActualUsageCostRow{}, nil
}
var rows []ActualUsageCostRow
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
/*
RAW SQL FOR RECORDING QUERY (untuk pengecekan database):
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(f.name, tf.name) AS flag_name,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) AS total_price,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) /
NULLIF(COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0), 0) AS average_price
FROM recordings r
JOIN recording_stocks rs ON rs.recording_id = r.id
JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN stock_allocations sa ON sa.usable_type = 'recording_stocks' AND sa.usable_id = rs.id AND sa.status = '<active_status>'
LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = '<purchase_stockable_key>'
LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = '<transfer_stockable_key>'
LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id
LEFT JOIN purchase_items tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id
LEFT JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'
LEFT JOIN flags tf ON tf.flagable_id = std.product_id AND tf.flagable_type = 'products'
WHERE r.project_flock_kandangs_id IN (<pfk_ids>)
AND r.deleted_at IS NULL
GROUP BY pw.product_id, p.name, COALESCE(f.name, tf.name)
*/
// Recording stock query (pakan, OVK, dll) dengan FIFO logic
recordingQuery := db.
Table("recordings AS r").
Select(`
pw.product_id,
p.name AS product_name,
COALESCE(f.name, tf.name) AS flag_name,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) AS total_price,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS qty_divisor,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) / NULLIF(COALESCE(SUM(
CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0), 0) AS average_price`,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey,
purchaseStockableKey, transferStockableKey).
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 products AS p ON p.id = pw.product_id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
"recording_stocks", entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN ?", pfkIDs).
Where("r.deleted_at IS NULL").
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
if err := recordingQuery.Scan(&rows).Error; err != nil {
return nil, err
}
/*
RAW SQL FOR CHICKIN QUERY (untuk pengecekan database):
SELECT
pw.product_id,
p.name AS product_name,
f.name AS flag_name,
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
FROM project_chickins pc
JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pc.product_warehouse_id
LEFT JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products'
WHERE pc.project_flock_kandang_id IN (<pfk_ids>)
AND pc.usage_qty > 0
GROUP BY pw.product_id, p.name, f.name
*/
// Chickin query (DOC, pullet) dengan FIFO sederhana
chickinQuery := db.
Table("project_chickins AS pc").
Select(`
pw.product_id,
p.name AS product_name,
f.name AS flag_name,
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
`).
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
Where("pc.usage_qty > 0").
Group("pw.product_id, p.name, f.name")
var chickinRows []ActualUsageCostRow
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
return nil, err
}
rows = append(rows, chickinRows...)
return rows, nil
}
// === EXPEDITION ===
// GetExpeditionHPP gets expedition HPP
func (r *ClosingKeuanganRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
db := r.DB().WithContext(ctx)
if projectFlockID == 0 {
return nil, fmt.Errorf("invalid project flock id")
}
var results []ExpeditionHPPRow
query := db.
Table("expense_realizations er").
Select(`
s.name AS supplier_name,
COALESCE(SUM(er.amount), 0) AS total_amount
`).
Joins("JOIN suppliers s ON s.id = er.supplier_id").
Where("er.category = 'EKSPEDISI'").
Where("er.deleted_at IS NULL")
if projectFlockKandangID != nil {
query = query.Where("er.project_flock_kandang_id = ?", *projectFlockKandangID)
} else {
query = query.Joins("JOIN project_flock_kandangs pfk ON pfk.id = er.project_flock_kandang_id").
Where("pfk.project_flock_id = ?", projectFlockID)
}
err := query.
Group("s.name").
Scan(&results).Error
if err != nil {
return nil, err
}
return results, nil
}
// === PRODUCTS ===
// GetProductsWithFlagsByIDs gets products with their flags
func (r *ClosingKeuanganRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
if len(productIDs) == 0 {
return []entity.Product{}, nil
}
var products []entity.Product
err := r.DB().WithContext(ctx).
Where("id IN ?", productIDs).
Preload("Flags", func(db *gorm.DB) *gorm.DB {
return db.Order("flagable_type, name")
}).
Find(&products).Error
if err != nil {
return nil, err
}
return products, nil
}
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]ProductUsageRow, error) {
if projectFlockKandangID == 0 {
return []ProductUsageRow{}, nil
}
var results []ProductUsageRow
rawQuery := `
SELECT
q.product_id,
q.product_name,
f.flag_names,
q.total_qty,
q.price,
q.total_qty * q.price AS total_pengeluaran
FROM (
SELECT
product_id,
product_name,
SUM(total_qty) AS total_qty,
AVG(price) AS price
FROM (
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0)
WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0)
WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0)
WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) AS price
FROM recordings r
JOIN recording_stocks rs ON rs.recording_id = r.id
JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'
LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'
LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'
LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'
LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'
LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
WHERE r.project_flock_kandangs_id = ?
AND r.deleted_at IS NULL
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM project_chickins pc
JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pc.project_flock_kandang_id = ?
AND pc.usage_qty > 0
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(mdp.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
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 products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(lts.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM laying_transfer_sources lts
JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id
JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(std.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM stock_transfer_details std
JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id
JOIN products p ON p.id = std.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(ads.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM adjustment_stocks ads
JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
AND ads.usage_qty > 0
GROUP BY pw.product_id, p.name
) x
GROUP BY product_id, product_name
) q
LEFT JOIN (
SELECT
p.id AS product_id,
STRING_AGG(DISTINCT f.name, ', ') AS flag_names
FROM products p
LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id
GROUP BY p.id
) f ON f.product_id = q.product_id
ORDER BY q.product_name
`
err := r.DB().WithContext(ctx).
Raw(rawQuery, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID).
Scan(&results).Error
if err != nil {
return nil, fmt.Errorf("failed to get all product usage: %w", err)
}
return results, nil
}