mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35d128cd52 | |||
| 824e96575a | |||
| d2f52b6901 |
@@ -0,0 +1,186 @@
|
||||
package dto
|
||||
|
||||
// New Closing Keuangan Response DTO Structure
|
||||
|
||||
// Base metrics - digunakan di banyak tempat
|
||||
type NewFinancialMetrics struct {
|
||||
RpPerBird float64 `json:"rp_per_bird"`
|
||||
RpPerKg float64 `json:"rp_per_kg"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// Comparison untuk Budgeting vs Realization
|
||||
type NewComparison struct {
|
||||
Budgeting NewFinancialMetrics `json:"budgeting"`
|
||||
Realization NewFinancialMetrics `json:"realization"`
|
||||
}
|
||||
|
||||
// HPP Purchase Section
|
||||
type HppPurchase struct {
|
||||
Pakan NewComparison `json:"pakan"`
|
||||
OVK NewComparison `json:"OVK"`
|
||||
DOC NewComparison `json:"DOC"`
|
||||
Depresiasi NewComparison `json:"Depresiasi"`
|
||||
}
|
||||
|
||||
// HPP Overhead Section
|
||||
type HppOverhead struct {
|
||||
Overhead NewComparison `json:"overhead"`
|
||||
Ekspedisi NewComparison `json:"ekspedisi"`
|
||||
}
|
||||
|
||||
// Summary HPP
|
||||
type NewSummaryHpp struct {
|
||||
Label string `json:"label"`
|
||||
Budgeting NewFinancialMetrics `json:"budgeting"`
|
||||
Realization NewFinancialMetrics `json:"realization"`
|
||||
EggBudgeting *NewFinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *NewFinancialMetrics `json:"egg_realization,omitempty"`
|
||||
}
|
||||
|
||||
// HPP wrapper
|
||||
type NewHpp struct {
|
||||
HppPurchase HppPurchase `json:"hpp_purchase"`
|
||||
HppOverhead HppOverhead `json:"hpp_overhead"`
|
||||
SummaryHpp NewSummaryHpp `json:"summary_hpp"`
|
||||
}
|
||||
|
||||
// Purchase Cost (dengan type field, embedded NewFinancialMetrics)
|
||||
type PurchaseCost struct {
|
||||
Type string `json:"type"`
|
||||
NewFinancialMetrics
|
||||
}
|
||||
|
||||
// PL Summary
|
||||
type PLSummary struct {
|
||||
GrossProfit NewFinancialMetrics `json:"gross_profit"`
|
||||
SubTotal NewFinancialMetrics `json:"sub_total"`
|
||||
NetProfit NewFinancialMetrics `json:"net_profit"`
|
||||
}
|
||||
|
||||
// Profit Loss wrapper
|
||||
type NewProfitLoss struct {
|
||||
PenjualanTelur NewFinancialMetrics `json:"penjualan_telur"`
|
||||
PurchaseCost PurchaseCost `json:"purchase_cost"`
|
||||
Overhead NewFinancialMetrics `json:"overhead"`
|
||||
Ekspedisi NewFinancialMetrics `json:"ekspedisi"`
|
||||
Summary PLSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// Main Data structure
|
||||
type NewClosingKeuanganData struct {
|
||||
Hpp NewHpp `json:"hpp"`
|
||||
ProfitLoss NewProfitLoss `json:"profit_loss"`
|
||||
}
|
||||
|
||||
// Full Response DTO
|
||||
type NewClosingKeuanganResponse struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data NewClosingKeuanganData `json:"data"`
|
||||
}
|
||||
|
||||
// === MAPPER FUNCTIONS ===
|
||||
|
||||
// ToNewFinancialMetrics creates a new financial metrics
|
||||
func ToNewFinancialMetrics(rpPerBird, rpPerKg, amount float64) NewFinancialMetrics {
|
||||
return NewFinancialMetrics{
|
||||
RpPerBird: rpPerBird,
|
||||
RpPerKg: rpPerKg,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// ToNewComparison creates a new budgeting vs realization comparison
|
||||
func ToNewComparison(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount, realizationRpPerBird, realizationRpPerKg, realizationAmount float64) NewComparison {
|
||||
return NewComparison{
|
||||
Budgeting: ToNewFinancialMetrics(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount),
|
||||
Realization: ToNewFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
||||
}
|
||||
}
|
||||
|
||||
// ToHppPurchase creates HPP purchase section
|
||||
func ToHppPurchase(pakan, oVK, dOC, depresiasi NewComparison) HppPurchase {
|
||||
return HppPurchase{
|
||||
Pakan: pakan,
|
||||
OVK: oVK,
|
||||
DOC: dOC,
|
||||
Depresiasi: depresiasi,
|
||||
}
|
||||
}
|
||||
|
||||
// ToHppOverhead creates HPP overhead section
|
||||
func ToHppOverhead(overhead, ekspedisi NewComparison) HppOverhead {
|
||||
return HppOverhead{
|
||||
Overhead: overhead,
|
||||
Ekspedisi: ekspedisi,
|
||||
}
|
||||
}
|
||||
|
||||
// ToNewSummaryHpp creates HPP summary
|
||||
func ToNewSummaryHpp(label string, budgeting, realization NewFinancialMetrics, eggBudgeting, eggRealization *NewFinancialMetrics) NewSummaryHpp {
|
||||
return NewSummaryHpp{
|
||||
Label: label,
|
||||
Budgeting: budgeting,
|
||||
Realization: realization,
|
||||
EggBudgeting: eggBudgeting,
|
||||
EggRealization: eggRealization,
|
||||
}
|
||||
}
|
||||
|
||||
// ToNewHpp creates complete HPP section
|
||||
func ToNewHpp(hppPurchase HppPurchase, hppOverhead HppOverhead, summaryHpp NewSummaryHpp) NewHpp {
|
||||
return NewHpp{
|
||||
HppPurchase: hppPurchase,
|
||||
HppOverhead: hppOverhead,
|
||||
SummaryHpp: summaryHpp,
|
||||
}
|
||||
}
|
||||
|
||||
// ToPurchaseCost creates purchase cost item
|
||||
func ToPurchaseCost(costType string, metrics NewFinancialMetrics) PurchaseCost {
|
||||
return PurchaseCost{
|
||||
Type: costType,
|
||||
NewFinancialMetrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
// ToPLSummary creates profit loss summary
|
||||
func ToPLSummary(grossProfit, subTotal, netProfit NewFinancialMetrics) PLSummary {
|
||||
return PLSummary{
|
||||
GrossProfit: grossProfit,
|
||||
SubTotal: subTotal,
|
||||
NetProfit: netProfit,
|
||||
}
|
||||
}
|
||||
|
||||
// ToNewProfitLoss creates complete profit loss section
|
||||
func ToNewProfitLoss(penjualanTelur, overhead, ekspedisi NewFinancialMetrics, purchaseCost PurchaseCost, summary PLSummary) NewProfitLoss {
|
||||
return NewProfitLoss{
|
||||
PenjualanTelur: penjualanTelur,
|
||||
PurchaseCost: purchaseCost,
|
||||
Overhead: overhead,
|
||||
Ekspedisi: ekspedisi,
|
||||
Summary: summary,
|
||||
}
|
||||
}
|
||||
|
||||
// ToNewClosingKeuanganData creates complete closing keuangan data
|
||||
func ToNewClosingKeuanganData(hpp NewHpp, profitLoss NewProfitLoss) NewClosingKeuanganData {
|
||||
return NewClosingKeuanganData{
|
||||
Hpp: hpp,
|
||||
ProfitLoss: profitLoss,
|
||||
}
|
||||
}
|
||||
|
||||
// ToSuccessNewClosingKeuanganResponse creates success response shortcut
|
||||
func ToSuccessNewClosingKeuanganResponse(data NewClosingKeuanganData) NewClosingKeuanganResponse {
|
||||
return NewClosingKeuanganResponse{
|
||||
Code: 200,
|
||||
Status: "success",
|
||||
Message: "Get closing keuangan successfully",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type ClosingModule struct{}
|
||||
|
||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
closingRepo := rClosing.NewClosingRepository(db)
|
||||
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
@@ -40,7 +41,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -24,7 +23,7 @@ type ClosingRepository interface {
|
||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
||||
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
||||
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
@@ -33,8 +32,6 @@ type ClosingRepository interface {
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
}
|
||||
|
||||
type ClosingRepositoryImpl struct {
|
||||
@@ -64,11 +61,6 @@ type SapronakRow struct {
|
||||
Notes string `gorm:"column:notes"`
|
||||
}
|
||||
|
||||
type ExpeditionHPPRow struct {
|
||||
SupplierName string `gorm:"column:supplier_name"`
|
||||
TotalAmount float64 `gorm:"column:total_amount"`
|
||||
}
|
||||
|
||||
type SapronakQueryParams struct {
|
||||
Type string
|
||||
WarehouseIDs []uint
|
||||
@@ -127,219 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
return rows, totalResults, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var purchaseAgg struct {
|
||||
TotalIn float64 `gorm:"column:total_in"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("purchase_items pi").
|
||||
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
||||
Scan(&purchaseAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var usageAgg struct {
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
}
|
||||
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("recording_stocks rs").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||
Scan(&usageAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectChickin{}).
|
||||
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(usage_qty), 0)").
|
||||
Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
Total float64 `gorm:"column:total_culling"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions rd").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", utils.FlagAyamCulling).
|
||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.Total, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs re").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
||||
if fcrID == 0 {
|
||||
return []entity.FcrStandard{}, nil
|
||||
}
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("fcr_id = ?", fcrID).
|
||||
Order("weight ASC").
|
||||
Find(&standards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return standards, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) 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")
|
||||
}
|
||||
|
||||
query := db.
|
||||
Table("expense_realizations AS er").
|
||||
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
|
||||
Joins("JOIN expenses e ON e.id = ens.expense_id").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
|
||||
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("e.category = ?", "BOP").
|
||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
||||
|
||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
||||
query = query.Where("pfk.id = ?", *projectFlockKandangID)
|
||||
}
|
||||
|
||||
var rows []ExpeditionHPPRow
|
||||
err := query.
|
||||
Select(
|
||||
"e.supplier_id AS supplier_id, " +
|
||||
"s.name AS supplier_name, " +
|
||||
"SUM(er.qty * er.price) AS total_amount",
|
||||
).
|
||||
Group("e.supplier_id, s.name").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
const (
|
||||
sapronakIncomingPurchasesSQL = `
|
||||
SELECT
|
||||
@@ -902,130 +681,180 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
return in, out, nil
|
||||
}
|
||||
|
||||
type ActualUsageCostRow struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
FlagName string `gorm:"column:flag_name"`
|
||||
// === CLOSING DATA PRODUKSI METHODS ===
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var purchaseAgg struct {
|
||||
TotalIn float64 `gorm:"column:total_in"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("purchase_items pi").
|
||||
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
||||
Scan(&purchaseAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var usageAgg struct {
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
}
|
||||
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("recording_stocks rs").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||
Scan(&usageAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectChickin{}).
|
||||
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(usage_qty), 0)").
|
||||
Scan(&total).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
Total float64 `gorm:"column:total_culling"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions rd").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", utils.FlagAyamCulling).
|
||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.Total, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
AveragePrice float64 `gorm:"column:average_price"`
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) 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
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs re").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
||||
if fcrID == 0 {
|
||||
return []entity.FcrStandard{}, nil
|
||||
}
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("fcr_id = ?", fcrID).
|
||||
Order("weight ASC").
|
||||
Find(&standards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pfkIDs) == 0 {
|
||||
return []ActualUsageCostRow{}, nil
|
||||
}
|
||||
|
||||
var rows []ActualUsageCostRow
|
||||
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||
|
||||
recordingQuery := db.
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
pw.product_id AS 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
|
||||
}
|
||||
|
||||
chickinQuery := db.
|
||||
Table("project_chickins AS pc").
|
||||
Select(`
|
||||
pw.product_id AS 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
|
||||
return standards, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
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
|
||||
}
|
||||
@@ -48,6 +48,7 @@ type closingService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ClosingKeuanganRepo repository.ClosingKeuanganRepository
|
||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
@@ -62,11 +63,12 @@ type closingService struct {
|
||||
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(repo repository.ClosingRepository, closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ClosingKeuanganRepo: closingKeuanganRepo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
MarketingRepo: marketingRepo,
|
||||
@@ -578,6 +580,7 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
||||
s.Log.Infof("🔵 [CLOSING KEUANGAN] Starting fetch for ProjectFlockID: %d", projectFlockID)
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||
@@ -589,23 +592,35 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
s.Log.Infof("✅ [CLOSING KEUANGAN] ProjectFlock fetched: ID=%d, Category=%s, FlockName=%s",
|
||||
projectFlock.Id, projectFlock.Category, projectFlock.FlockName)
|
||||
|
||||
// Validasi: Closing Keuangan hanya untuk LAYING, bukan GROWING
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
s.Log.Warnf("⚠️ [CLOSING KEUANGAN] ProjectFlock ID %d is GROWING category, closing keuangan not available", projectFlockID)
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Closing keuangan only available for LAYING category")
|
||||
}
|
||||
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||
}
|
||||
s.Log.Infof("💰 [CLOSING KEUANGAN] Budgets fetched: %d records", len(budgets))
|
||||
|
||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||
actualUsageRows, err := s.ClosingKeuanganRepo.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
||||
}
|
||||
s.Log.Infof("📊 [CLOSING KEUANGAN] Actual Usage Costs fetched: %d records", len(actualUsageRows))
|
||||
|
||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
||||
s.Log.Infof("🛒 [CLOSING KEUANGAN] Converted to Purchase Items: %d items", len(purchaseItems))
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||
}
|
||||
s.Log.Infof("💸 [CLOSING KEUANGAN] Expense Realizations fetched: %d records", len(realizations))
|
||||
|
||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("MarketingProduct").
|
||||
@@ -615,26 +630,31 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||
}
|
||||
s.Log.Infof("🚚 [CLOSING KEUANGAN] Marketing Delivery Products fetched: %d records", len(deliveryProducts))
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||
}
|
||||
s.Log.Infof("🐣 [CLOSING KEUANGAN] Chickins fetched: %d records", len(chickins))
|
||||
|
||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
||||
}
|
||||
s.Log.Infof("⚖️ [CLOSING KEUANGAN] Total Weight Produced: %.2f kg", totalWeightProduced)
|
||||
|
||||
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
||||
}
|
||||
s.Log.Infof("🥚 [CLOSING KEUANGAN] Total Egg Weight: %.2f kg", totalEggWeightKg)
|
||||
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
}
|
||||
s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion)
|
||||
|
||||
input := dto.ClosingKeuanganInput{
|
||||
ProjectFlockCategory: projectFlock.Category,
|
||||
@@ -650,6 +670,7 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
|
||||
|
||||
report := dto.ToClosingKeuanganReport(input)
|
||||
|
||||
s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID)
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
@@ -658,7 +679,7 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
rows, err := s.ClosingKeuanganRepo.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
|
||||
@@ -1115,7 +1136,7 @@ func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, a
|
||||
}
|
||||
|
||||
// Fetch products with flags from repository
|
||||
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs)
|
||||
products, err := s.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
|
||||
products = []entity.Product{}
|
||||
|
||||
Reference in New Issue
Block a user