diff --git a/internal/common/repository/common.hpp.repository.go b/internal/common/repository/common.hpp.repository.go index c005e24e..da4b1908 100644 --- a/internal/common/repository/common.hpp.repository.go +++ b/internal/common/repository/common.hpp.repository.go @@ -20,7 +20,7 @@ type HppCostRepository interface { GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) - GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) + GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) } @@ -196,10 +196,10 @@ func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKanda } func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) { - if date == nil { - now := time.Now() - date = &now - } + // if date == nil { + // now := time.Now() + // date = &now + // } var totals struct { TotalPieces float64 @@ -222,12 +222,13 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds( ctx context.Context, projectFlockKandangIDs []uint, - date *time.Time, + startDate *time.Time, + endDate *time.Time, ) (float64, float64, error) { - if date == nil { + if endDate == nil { now := time.Now() - date = &now + endDate = &now } type subResult struct { @@ -251,7 +252,8 @@ func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangI ). Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id"). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). - Where("r.record_datetime <= ?", *date) + Where("r.record_datetime <= ?", *endDate). + Where("mdp.delivery_date = ?", *startDate) var totals struct { TotalPieces float64 diff --git a/internal/common/service/common.hpp.service.go b/internal/common/service/common.hpp.service.go index 44f2dd5f..b1f1a1b1 100644 --- a/internal/common/service/common.hpp.service.go +++ b/internal/common/service/common.hpp.service.go @@ -11,10 +11,10 @@ import ( type HppService interface { CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) - GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, totalDepresiasiGrowing float64) (float64, error) - GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error) + GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) + GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error) - GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) + GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) } type HppCostResponse struct { @@ -44,17 +44,25 @@ func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Tim date = &now } - depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, date) + location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, err } - totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, date, depresiasiTransfer) + startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) + endOfDay := startOfDay.Add(24 * time.Hour) + + depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, date) + totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) + if err != nil { + return nil, err + } + + return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) } @@ -101,23 +109,23 @@ func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, d return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil } -func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, date *time.Time, depresiasiTransfer float64) (float64, error) { - if date == nil { - now := time.Now() - date = &now - } +func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { + // if date == nil { + // now := time.Now() + // date = &now + // } costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) if err != nil { return 0, err } - costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, date) + costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return 0, err } - costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, date) + costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return 0, err } @@ -127,7 +135,7 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, date *ti return 0, err } - costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, date) + costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) if err != nil { return 0, err } @@ -135,11 +143,11 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, date *ti return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil } -func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, date *time.Time) (float64, error) { - if date == nil { - now := time.Now() - date = &now - } +func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { + // if date == nil { + // now := time.Now() + // date = &now + // } if s.hppRepo == nil { return 0, nil @@ -155,12 +163,12 @@ func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, date *ti return 0, err } - eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, date) + eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) if err != nil { return 0, err } - eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date) + eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return 0, err } @@ -177,11 +185,11 @@ func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, date *ti return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil } -func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error) { - if date == nil { - now := time.Now() - date = &now - } +func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { + // if endDate == nil { + // now := time.Now() + // endDate = &now + // } if s.hppRepo == nil { return 0, nil @@ -205,7 +213,7 @@ func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, date *tim return 0, nil } - totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, date) + totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) if err != nil { return 0, err } @@ -213,22 +221,18 @@ func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, date *tim return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil } -func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { - if date == nil { - now := time.Now() - date = &now - } +func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { if s.hppRepo == nil { return &HppCostResponse{}, nil } - estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date) + estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return nil, err } - realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, date) + realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { return nil, err } diff --git a/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql new file mode 100644 index 00000000..e8f203a8 --- /dev/null +++ b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE adjustment_stocks ADD COLUMN stock_log_id INTEGER; + +CREATE INDEX idx_adjustment_stocks_stock_log_id ON adjustment_stocks (stock_log_id); \ No newline at end of file diff --git a/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql new file mode 100644 index 00000000..32f27161 --- /dev/null +++ b/internal/database/migrations/20260126075643_remove_stock_log_id_from_adjustment_stocks.up.sql @@ -0,0 +1 @@ +ALTER TABLE adjustment_stocks DROP COLUMN IF EXISTS stock_log_id; \ No newline at end of file diff --git a/internal/entities/adjustment_stock.go b/internal/entities/adjustment_stock.go index ef27d0c2..841e4820 100644 --- a/internal/entities/adjustment_stock.go +++ b/internal/entities/adjustment_stock.go @@ -4,7 +4,6 @@ import "time" type AdjustmentStock struct { Id uint `gorm:"primaryKey"` - StockLogId uint `gorm:"column:stock_log_id;not null;index"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` TotalQty float64 `gorm:"column:total_qty;default:0"` TotalUsed float64 `gorm:"column:total_used;default:0"` @@ -13,6 +12,6 @@ type AdjustmentStock struct { CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` - StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"` } diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 6ca19d5c..a7238b17 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,8 +1,12 @@ package dto -// === CLOSING KEUANGAN CODES === +import ( + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) -// Closing HPP Codes type ClosingHPPCode string const ( @@ -14,36 +18,30 @@ const ( HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI" ) -// Closing Profit Loss Codes type ClosingProfitLossCode string const ( - PLCodeSales ClosingProfitLossCode = "SALES" - PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" - PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" + PLCodeSales ClosingProfitLossCode = "SALES" + PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" + PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI" ) -// === NEW CLOSING KEUANGAN DTO === - -// FinancialMetrics represents financial metrics with per unit and total amounts type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` Amount float64 `json:"amount"` } -// HPPItem represents an item in HPP section type HPPItem struct { ID uint `json:"id"` - Category string `json:"category"` // "purchase" or "overhead" - Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI" + Category string `json:"category"` + Code string `json:"code"` Label string `json:"label"` Budgeting FinancialMetrics `json:"budgeting"` Realization FinancialMetrics `json:"realization"` } -// HPPSummary represents summary for HPP section type HPPSummary struct { Label string `json:"label"` Budgeting FinancialMetrics `json:"budgeting"` @@ -52,52 +50,41 @@ type HPPSummary struct { EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` } -// HPPSection represents HPP data section type HPPSection struct { - Items []HPPItem `json:"items"` + Items []HPPItem `json:"items"` Summary HPPSummary `json:"summary"` } -// ProfitLossItem represents an item in Profit & Loss section type ProfitLossItem struct { - Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI" + Code string `json:"code"` Label string `json:"label"` - Type string `json:"type"` // "income", "purchase", "overhead" + Type string `json:"type"` RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` Amount float64 `json:"amount"` } -// ProfitLossSummary represents summary for Profit & Loss section type ProfitLossSummary struct { GrossProfit FinancialMetrics `json:"gross_profit"` SubTotal FinancialMetrics `json:"sub_total"` NetProfit FinancialMetrics `json:"net_profit"` } -// ProfitLossSection represents Profit & Loss data section type ProfitLossSection struct { - Items []ProfitLossItem `json:"items"` - Summary ProfitLossSummary `json:"summary"` + Items []ProfitLossItem `json:"items"` + Summary ProfitLossSummary `json:"summary"` } -// ClosingKeuanganData represents the main data structure type ClosingKeuanganData struct { - HPP HPPSection `json:"hpp"` + HPP HPPSection `json:"hpp"` ProfitLoss ProfitLossSection `json:"profit_loss"` } - -// ClosingKeuanganResponse represents the full API response -type ClosingKeuanganResponse struct { - Code int `json:"code"` - Status string `json:"status"` - Message string `json:"message"` - Data ClosingKeuanganData `json:"data"` +type MetricsCalculator struct { + TotalPopulation float64 + ActualPopulation float64 + TotalWeightProduced float64 } -// === MAPPER FUNCTIONS === - -// ToFinancialMetrics creates FinancialMetrics from values func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { return FinancialMetrics{ RpPerBird: rpPerBird, @@ -106,7 +93,6 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { } } -// ToHPPItem creates HPP item func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem { return HPPItem{ ID: id, @@ -118,7 +104,6 @@ func ToHPPItem(id uint, category, code, label string, budgeting, realization Fin } } -// ToHPPSummary creates HPP summary func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary { return HPPSummary{ Label: label, @@ -129,7 +114,6 @@ func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudg } } -// ToHPPSection creates HPP section func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { return HPPSection{ Items: items, @@ -137,7 +121,6 @@ func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { } } -// ToProfitLossItem creates Profit & Loss item func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem { return ProfitLossItem{ Code: code, @@ -149,7 +132,6 @@ func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount f } } -// ToProfitLossSummary creates Profit & Loss summary func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary { return ProfitLossSummary{ GrossProfit: grossProfit, @@ -158,7 +140,6 @@ func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) Prof } } -// ToProfitLossSection creates Profit & Loss section func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection { return ProfitLossSection{ Items: items, @@ -166,7 +147,6 @@ func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) Prof } } -// ToClosingKeuanganData creates complete closing keuangan data func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData { return ClosingKeuanganData{ HPP: hpp, @@ -174,12 +154,72 @@ func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) Closing } } -// ToSuccessClosingKeuanganResponse creates success response -func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse { - return ClosingKeuanganResponse{ - Code: 200, - Status: "success", - Message: "Get closing keuangan successfully", - Data: data, +func (mc *MetricsCalculator) CalculateMetrics(amount float64) (rpPerBird, rpPerKg float64) { + if mc.ActualPopulation > 0 { + rpPerBird = amount / mc.ActualPopulation } + if mc.TotalWeightProduced > 0 { + rpPerKg = amount / mc.TotalWeightProduced + } + return +} + +func (mc *MetricsCalculator) CalculateProfitLossMetrics(amount float64) (rpPerBird, rpPerKg float64) { + if mc.TotalPopulation > 0 { + rpPerBird = amount / mc.TotalPopulation + } + if mc.TotalWeightProduced > 0 { + rpPerKg = amount / mc.TotalWeightProduced + } + return +} + +type ProductFilter struct { + ProjectFlockCategory string +} + +func (pf *ProductFilter) IsEggProduct(product entity.Product) bool { + for _, flag := range product.Flags { + flagName := strings.ToUpper(flag.Name) + if flagName == string(utils.FlagTelur) || + flagName == string(utils.FlagTelurUtuh) || + flagName == string(utils.FlagTelurPecah) || + flagName == string(utils.FlagTelurPutih) || + flagName == string(utils.FlagTelurRetak) { + return true + } + } + return false +} + +func (pf *ProductFilter) IsChickenProduct(product entity.Product) bool { + for _, flag := range product.Flags { + flagName := strings.ToUpper(flag.Name) + if flagName == string(utils.FlagAyamAfkir) || + flagName == string(utils.FlagAyamCulling) || + flagName == string(utils.FlagAyamMati) { + return true + } + } + return false +} + +func (pf *ProductFilter) ShouldIncludeProduct(product entity.Product) bool { + if pf.ProjectFlockCategory == string(utils.ProjectFlockCategoryLaying) { + return pf.IsEggProduct(product) + } + return pf.IsChickenProduct(product) || (!pf.IsEggProduct(product) && !pf.IsChickenProduct(product)) +} + +func (pf *ProductFilter) FilterDeliveryProducts(deliveries []entity.MarketingDeliveryProduct) []entity.MarketingDeliveryProduct { + filtered := make([]entity.MarketingDeliveryProduct, 0) + for _, delivery := range deliveries { + if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { + continue + } + if pf.ShouldIncludeProduct(delivery.MarketingProduct.ProductWarehouse.Product) { + filtered = append(filtered, delivery) + } + } + return filtered } diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index 72523b69..a4bb5cb0 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -8,6 +8,7 @@ import ( customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === Response DTO === @@ -49,7 +50,12 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { productFlags[i] = f.Name } - ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags) + var category string + if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil { + category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category + } + + ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -131,14 +137,27 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua } } -func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string) (int, int) { +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) { if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { return 0, 0 } for _, flag := range productFlags { - if flag == "OVK" || flag == "PAKAN" { - return 0, 0 // + if flag == string(utils.FlagOVK) || + flag == string(utils.FlagPakan) || + flag == string(utils.FlagPreStarter) || + flag == string(utils.FlagStarter) || + flag == string(utils.FlagFinisher) || + flag == string(utils.FlagObat) || + flag == string(utils.FlagVitamin) || + flag == string(utils.FlagKimia) || + flag == string(utils.FlagEkspedisi) || + flag == string(utils.FlagTelur) || + flag == string(utils.FlagTelurUtuh) || + flag == string(utils.FlagTelurPecah) || + flag == string(utils.FlagTelurPutih) || + flag == string(utils.FlagTelurRetak) { + return 0, 0 } } @@ -156,8 +175,12 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de if ageInDays <= 0 { ageInWeeks = 0 } else { - - ageInWeeks = ((ageInDays - 1) / 7) + 1 + if category == string(utils.ProjectFlockCategoryLaying) { + ageInDays = ageInDays + 119 + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } else { + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } } return ageInDays, ageInWeeks diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 81fe7ebd..92d3b2ee 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -196,7 +196,11 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin } for idx, item := range group.Items { - productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + item.NoReferensi + "|" + formatDate(item.Tanggal)) + refKey := strings.TrimSpace(item.NoReferensi) + productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey) + if refKey == "" { + productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal)) + } baseRow := SapronakCategoryRowDTO{ ID: idx + 1, Date: formatDate(item.Tanggal), @@ -212,6 +216,9 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin switch strings.ToLower(item.JenisTransaksi) { case "pembelian", "adjustment masuk", "mutasi masuk": row.QtyIn += item.QtyMasuk + if item.Tanggal != nil { + row.Date = formatDate(item.Tanggal) + } if row.UnitPrice == 0 { if item.QtyMasuk > 0 && item.Nilai > 0 { row.UnitPrice = item.Nilai / item.QtyMasuk diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index 1079663d..666c055d 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -25,7 +25,6 @@ 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,9 +39,11 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) + hppCostRepo := commonRepo.NewHppCostRepository(db) + hppService := commonSvc.NewHppService(hppCostRepo) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) - closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo) + closingKeuanganService := sClosing.NewClosingKeuanganService(projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo, hppService, hppCostRepo) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index daff5d35..82e6f4a7 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -709,6 +709,23 @@ var ( sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet) ) +func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB { + subquery := r.DB(). + Table("flags"). + Select("DISTINCT ON (flagable_id) flagable_id, name"). + Where("flagable_type = ?", entity.FlagableTypeProduct). + Where("name IN ?", sapronakFlagsAll). + Order(fmt.Sprintf( + "flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END", + utils.FlagDOC, + utils.FlagPullet, + utils.FlagPakan, + utils.FlagOVK, + )) + + return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery) +} + func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow { m := make(map[uint][]SapronakDetailRow) for _, row := range rows { @@ -745,11 +762,12 @@ func (r *ClosingRepositoryImpl) usageQuery( COALESCE(p.product_price, 0) AS default_price `) db = applyJoins(db, joins...) - return db. + db = db. Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where(where, args...) + db = r.joinSapronakProductFlag(db, "p") + return db } func (r *ClosingRepositoryImpl) fetchSapronakUsage( @@ -780,10 +798,10 @@ func (r *ClosingRepositoryImpl) detailQuery( db := r.withCtx(ctx). Table(table). Joins("JOIN product_warehouses pw ON "+pwJoinCond). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct) + Joins("JOIN products p ON p.id = pw.product_id") db = applyJoins(db, joins...) + db = r.joinSapronakProductFlag(db, "p") return db.Select(selectSQL).Where(where, args...) } @@ -907,7 +925,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C `). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()). Joins("LEFT JOIN recordings r ON r.id = rs.recording_id"). Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()). @@ -930,7 +947,8 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C `, fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, - ). + ) + query = r.joinSapronakProductFlag(query, "p"). Group(` pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, sl.created_at, pc.chick_in_date, r.record_datetime, @@ -942,15 +960,15 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C } func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB { - return r.withCtx(ctx). + db := r.withCtx(ctx). Table("purchase_items AS pi"). Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL"). Joins("JOIN products p ON p.id = pi.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN warehouses w ON w.id = pi.warehouse_id"). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). Where("pi.received_date IS NOT NULL") + return r.joinSapronakProductFlag(db, "p") } func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) { @@ -1021,10 +1039,10 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui `). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN warehouses w ON w.id = pw.warehouse_id") db = applyJoins(db, joins...) + db = r.joinSapronakProductFlag(db, "p") if err := db. Where("sl.loggable_type = ?", logType). @@ -1093,10 +1111,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("w.kandang_id = ?", kandangID). Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) + incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p") incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err @@ -1121,10 +1139,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("w.kandang_id = ?", kandangID). Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) + incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p") incomingLaying, err := scanAndGroupDetails(incomingLayingQuery) if err != nil { return nil, nil, err @@ -1152,12 +1170,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id"). Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("w.kandang_id = ?", kandangID). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") + outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoing, err := scanAndGroupDetails(outgoingQuery) if err != nil { return nil, nil, err @@ -1183,12 +1201,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("w.kandang_id = ?", kandangID). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price") + outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p") outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery) if err != nil { return nil, nil, err @@ -1218,12 +1236,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") + query = r.joinSapronakProductFlag(query, "p") sales, err := scanAndGroupDetails(query) if err != nil { return nil, err @@ -1245,7 +1263,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ?", fifo.UsableKeyMarketingDelivery.String(), entity.StockAllocationStatusActive, @@ -1256,6 +1273,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") + nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p") nonFifoSales, err := scanAndGroupDetails(nonFifoQuery) if err != nil { return nil, err diff --git a/internal/modules/closings/repositories/closingKeuangan.repository.go b/internal/modules/closings/repositories/closingKeuangan.repository.go deleted file mode 100644 index dedea807..00000000 --- a/internal/modules/closings/repositories/closingKeuangan.repository.go +++ /dev/null @@ -1,365 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "sort" - "strings" - - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -// ClosingKeuanganRepository handles database operations for closing keuangan -type ClosingKeuanganRepository interface { - repository.BaseRepository[interface{}] - - // All Product Usage - GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) - - // Depletion per kandang - GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) - - // Weight produced from uniformity per kandang - GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) - - // DB returns the underlying GORM DB instance - DB() *gorm.DB -} - -type ClosingKeuanganRepositoryImpl struct { - *repository.BaseRepositoryImpl[interface{}] -} - -func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository { - return &ClosingKeuanganRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db), - } -} - -// Result Rows - -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"` -} - -// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang -// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments -// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all -func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) { - if projectFlockKandangID == 0 { - return []ProductUsageRow{}, nil - } - - type SubQueryResult struct { - ProductID uint `gorm:"column:product_id"` - ProductName string `gorm:"column:product_name"` - TotalQty float64 `gorm:"column:total_qty"` - Price float64 `gorm:"column:price"` - } - - type AggregatedResult struct { - ProductID uint `gorm:"column:product_id"` - ProductName string `gorm:"column:product_name"` - TotalQty float64 `gorm:"column:total_qty"` - Price float64 `gorm:"column:price"` - PriceCount int `gorm:"-"` // For calculating average price - } - - type FlagResult struct { - ProductID uint `gorm:"column:product_id"` - FlagNames string `gorm:"column:flag_names"` - } - - var allResults []SubQueryResult - - // Subquery 1: Recordings - var recordingsResults []SubQueryResult - err := r.DB().WithContext(ctx). - Table("recordings r"). - 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"). - Joins("JOIN recording_stocks rs ON rs.recording_id = r.id"). - Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'"). - Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'"). - Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'"). - Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'"). - Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'"). - Joins("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 = ?", projectFlockKandangID). - Where("r.deleted_at IS NULL"). - Group("pw.product_id, p.name"). - Scan(&recordingsResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get recordings product usage: %w", err) - } - fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID) - allResults = append(allResults, recordingsResults...) - - // Subquery 2: Chickins - var chickinsResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("project_chickins pc"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pc.project_flock_kandang_id = ?", projectFlockKandangID). - Where("pc.usage_qty > 0"). - Group("pw.product_id, p.name"). - Scan(&chickinsResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get chickins product usage: %w", err) - } - fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID) - allResults = append(allResults, chickinsResults...) - - // Subquery 3: Marketing Delivery - var marketingResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("marketing_delivery_products mdp"). - 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"). - Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). - Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&marketingResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get marketing product usage: %w", err) - } - fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID) - allResults = append(allResults, marketingResults...) - - // Subquery 4: Laying Transfer Sources - var layingTransferResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("laying_transfer_sources lts"). - 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"). - Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id"). - Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&layingTransferResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err) - } - fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID) - allResults = append(allResults, layingTransferResults...) - - // Subquery 5: Stock Transfer Details - var stockTransferResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("stock_transfer_details std"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id"). - Joins("JOIN products p ON p.id = std.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&stockTransferResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err) - } - fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID) - allResults = append(allResults, stockTransferResults...) - - // Subquery 6: Adjustment Stocks - var adjustmentResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("adjustment_stocks ads"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Where("ads.usage_qty > 0"). - Group("pw.product_id, p.name"). - Scan(&adjustmentResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get adjustment product usage: %w", err) - } - fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID) - allResults = append(allResults, adjustmentResults...) - - fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults)) - - // Aggregate results by product_id - aggregatedMap := make(map[uint]*AggregatedResult) - for _, result := range allResults { - key := result.ProductID - if existing, exists := aggregatedMap[key]; exists { - existing.TotalQty += result.TotalQty - existing.Price += result.Price - existing.PriceCount++ - } else { - aggregatedMap[key] = &AggregatedResult{ - ProductID: result.ProductID, - ProductName: result.ProductName, - TotalQty: result.TotalQty, - Price: result.Price, - PriceCount: 1, - } - } - } - - fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap)) - - // Get flags for all products - productIDs := make([]uint, 0, len(aggregatedMap)) - for id := range aggregatedMap { - productIDs = append(productIDs, id) - } - - var flagResults []FlagResult - if len(productIDs) > 0 { - err = r.DB().WithContext(ctx). - Table("products p"). - Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names"). - Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id"). - Where("p.id IN ?", productIDs). - Group("p.id"). - Scan(&flagResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get product flags: %w", err) - } - } - fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults)) - - // Build flag map - flagMap := make(map[uint]string) - for _, flag := range flagResults { - flagMap[flag.ProductID] = flag.FlagNames - } - - // Combine results and calculate average price - results := make([]ProductUsageRow, 0, len(aggregatedMap)) - for _, agg := range aggregatedMap { - avgPrice := float64(0) - if agg.PriceCount > 0 { - avgPrice = agg.Price / float64(agg.PriceCount) - } - - flagNames := flagMap[agg.ProductID] - - // Apply flag filters if provided - if len(flagFilters) > 0 { - // Check if any of the flagFilters exist in flagNames - matched := false - for _, filter := range flagFilters { - if containsIgnoreCase(flagNames, filter) { - matched = true - break - } - } - if !matched { - continue // Skip this product if no flag matches - } - } - - results = append(results, ProductUsageRow{ - ProductID: agg.ProductID, - ProductName: agg.ProductName, - FlagNames: flagNames, - TotalQty: agg.TotalQty, - Price: avgPrice, - TotalPengeluaran: agg.TotalQty * avgPrice, - }) - } - - fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results)) - for i, r := range results { - fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n", - i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran) - } - - // Sort by product name - sort.Slice(results, func(i, j int) bool { - return results[i].ProductName < results[j].ProductName - }) - - fmt.Printf("[REPO] Final sorted results: %d items\n", len(results)) - return results, nil -} - -// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang -func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { - var result float64 - err := r.DB().WithContext(ctx). - Table("recording_depletions"). - Select("COALESCE(SUM(recording_depletions.qty), 0)"). - Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). - Where("project_flock_kandangs.id = ?", projectFlockKandangID). - Scan(&result).Error - return result, err -} - -// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang -// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000 -func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { - if projectFlockKandangID == 0 { - return 0, nil - } - - var uniformity struct { - MeanUp float64 - ChickQtyOfWeight float64 - } - - err := r.DB().WithContext(ctx). - Table("project_flock_kandang_uniformity"). - Select("mean_up, chick_qty_of_weight"). - Where("project_flock_kandang_id = ?", projectFlockKandangID). - Order("id DESC"). - Limit(1). - Scan(&uniformity).Error - - if err != nil { - return 0, err - } - - // Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000 - totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000 - - return totalWeight, nil -} - -// containsIgnoreCase checks if a string contains a substring (case-insensitive) -func containsIgnoreCase(str, substr string) bool { - return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr)) -} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 02942f44..5494a835 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -162,7 +162,12 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { - realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) + if err != nil { + return nil, err + } + + realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID, projectFlock.Category) if err != nil { return nil, err } diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index 0f3351f7..85aa5f1c 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -2,20 +2,19 @@ package service import ( "errors" - "strings" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" - repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "github.com/gofiber/fiber/v2" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -25,9 +24,28 @@ type ClosingKeuanganService interface { GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) } +// CostData holds all cost-related information +type CostData struct { + FeedCost float64 + OvkCost float64 + ChickenCost float64 + ExpeditionCost float64 + BudgetOperational float64 + RealizationOperational float64 +} + +// ProductionData holds all production and sales related information +type ProductionData struct { + TotalPopulationIn float64 + TotalDepletion float64 + TotalWeightProduced float64 + TotalEggWeightKg float64 + TotalWeightSold float64 + TotalSalesAmount float64 +} + type closingKeuanganService struct { Log *logrus.Logger - ClosingKeuanganRepo repository.ClosingKeuanganRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository @@ -35,10 +53,11 @@ type closingKeuanganService struct { ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository RecordingRepo recordingRepository.RecordingRepository + HppSvc commonSvc.HppService + HppRepo commonRepo.HppCostRepository } func NewClosingKeuanganService( - closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, @@ -46,10 +65,11 @@ func NewClosingKeuanganService( projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, recordingRepo recordingRepository.RecordingRepository, + hppSvc commonSvc.HppService, + hppRepo commonRepo.HppCostRepository, ) ClosingKeuanganService { return &closingKeuanganService{ Log: utils.Log, - ClosingKeuanganRepo: closingKeuanganRepo, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, @@ -57,6 +77,8 @@ func NewClosingKeuanganService( ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, + HppSvc: hppSvc, + HppRepo: hppRepo, } } @@ -73,30 +95,12 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - 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") - } - - // Preload Nonstock.Flags manually - var budgetIDs []uint - for _, b := range budgets { - budgetIDs = append(budgetIDs, b.Id) - } - if len(budgetIDs) > 0 { - err = s.ProjectBudgetRepo.DB().WithContext(c.Context()). - Preload("Nonstock.Flags"). - Where("id IN ?", budgetIDs). - Find(&budgets).Error - } - - // Get all kandang for this project flock - kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) + projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } - return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) + return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) } func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) { @@ -107,12 +111,11 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec return nil, err } - // Validate and fetch project flock kandang - kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") } - if kandang.ProjectFlockId != projectFlockID { + if projectFlockKandang.ProjectFlockId != projectFlockID { return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock") } @@ -121,417 +124,249 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - 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") - } + projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang} - // Preload Nonstock.Flags manually - var budgetIDs []uint - for _, b := range budgets { - budgetIDs = append(budgetIDs, b.Id) - } - if len(budgetIDs) > 0 { - err = s.ProjectBudgetRepo.DB().WithContext(c.Context()). - Preload("Nonstock.Flags"). - Where("id IN ?", budgetIDs). - Find(&budgets).Error - } - - kandangs := []entity.ProjectFlockKandang{*kandang} - - return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) + return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) } -func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) { - // Define flag filters using constants - pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)} - ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)} - ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)} - allFilters := append(pakanFilters, ovkFilters...) - allFilters = append(allFilters, ayamFilters...) +func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang) (*dto.ClosingKeuanganData, error) { - var allProductUsageRows []repository.ProductUsageRow - - // Get ALL product usage - for _, kandang := range kandangs { - rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters) - if err == nil { - allProductUsageRows = append(allProductUsageRows, rows...) - } + var projectFlockKandangIDs []uint + for _, projectFlockKandang := range projectFlockKandangs { + projectFlockKandangIDs = append(projectFlockKandangIDs, projectFlockKandang.Id) } - // Classify into categories based on flag priority - var pakanProductUsageRows []repository.ProductUsageRow - var ovkProductUsageRows []repository.ProductUsageRow - var ayamProductUsageRows []repository.ProductUsageRow - - for _, row := range allProductUsageRows { - // Parse flag names from comma-separated string - flagNames := strings.Split(row.FlagNames, ",") - - hasPakanFlag := false - hasOvkFlag := false - hasAyamFlag := false - - for _, flag := range flagNames { - flag = strings.TrimSpace(flag) - if containsItem(pakanFilters, flag) { - hasPakanFlag = true - } - if containsItem(ovkFilters, flag) { - hasOvkFlag = true - } - if containsItem(ayamFilters, flag) { - hasAyamFlag = true - } - } - - // Priority: PAKAN > OVK > AYAM - if hasPakanFlag { - pakanProductUsageRows = append(pakanProductUsageRows, row) - } else if hasOvkFlag { - ovkProductUsageRows = append(ovkProductUsageRows, row) - } else if hasAyamFlag { - ayamProductUsageRows = append(ayamProductUsageRows, row) - } else { - continue - } - } - - - // Calculate total price for each category - var totalPakanPrice, totalOvkPrice, totalAyamPrice float64 - for _, row := range pakanProductUsageRows { - totalPakanPrice += row.TotalPengeluaran - } - for _, row := range ovkProductUsageRows { - totalOvkPrice += row.TotalPengeluaran - } - for _, row := range ayamProductUsageRows { - totalAyamPrice += row.TotalPengeluaran - } - - // Determine if this is per-kandang or per-project-flock scope - isPerKandang := len(kandangs) == 1 + isPerKandang := len(projectFlockKandangs) == 1 var projectFlockKandangID *uint if isPerKandang { - kandangID := kandangs[0].Id + kandangID := projectFlockKandangs[0].Id projectFlockKandangID = &kandangID } + costs, err := s.calculateCosts(c, projectFlock, projectFlockKandangs, projectFlockKandangIDs, projectFlockKandangID) + if err != nil { + return nil, err + } + + productionData, err := s.calculateProductionData(c, projectFlock, projectFlockKandangIDs, projectFlockKandangID) + if err != nil { + return nil, err + } + + hppSection := s.buildHPPSection(c, projectFlock, projectFlockKandangs, costs, productionData) + + profitLossSection := s.buildProfitLossSection(projectFlock, costs, productionData) + + data := dto.ToClosingKeuanganData(hppSection, profitLossSection) + return &data, nil +} + +func (s closingKeuanganService) calculateCosts(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*CostData, error) { + costs := &CostData{} var err error - // Fetch realizations - var realizations []entity.ExpenseRealization - if isPerKandang && projectFlockKandangID != nil { - realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID) - } else { - realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil) - } + costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") + costs.FeedCost = 0 } - deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB { - db = db.Preload("MarketingProduct"). - Preload("MarketingProduct.ProductWarehouse"). - Preload("MarketingProduct.ProductWarehouse.Product") - return db - }) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") - } - - // Filter by kandang if scope is per-kandang (manual filtering after fetch) - if isPerKandang && projectFlockKandangID != nil { - filteredProducts := make([]entity.MarketingDeliveryProduct, 0) - for _, dp := range deliveryProducts { - pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId - if pfKandangID != nil && *pfKandangID == *projectFlockKandangID { - filteredProducts = append(filteredProducts, dp) - } - } - deliveryProducts = filteredProducts - } - - // Fetch chickins - var chickins []entity.ProjectChickin - if isPerKandang && projectFlockKandangID != nil { - chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) - } + costs.OvkCost, err = s.HppRepo.GetOvkUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") + costs.OvkCost = 0 } - // Get total depletion - var totalDepletion float64 - if isPerKandang && projectFlockKandangID != nil { - totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) - } - if err != nil { - totalDepletion = 0 - } - - totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id) - if err != nil { - } - - // Try to get actual weight from uniformity data - var totalWeightFromUniformity float64 - if isPerKandang && projectFlockKandangID != nil { - totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) - } - if err != nil { - } else if totalWeightFromUniformity > 0 { - totalWeightProduced = totalWeightFromUniformity - } - - // Fetch egg data only for Laying category - var totalEggWeightKg float64 if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - // TODO: Replace with actual method to get egg weight from RecordingRepo - // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id) - // For now, set to 0 as placeholder - totalEggWeightKg = 0 + for _, projectFlockKandang := range projectFlockKandangs { + depresiasiCost, err := s.HppSvc.GetDepresiasiTransfer(projectFlockKandang.Id, nil) + if err == nil { + costs.ChickenCost += depresiasiCost + } + pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) + if err == nil { + costs.ChickenCost += pulletCost + } + } } else { - totalEggWeightKg = 0 + for _, projectFlockKandang := range projectFlockKandangs { + pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) + if err == nil { + costs.ChickenCost += pulletCost + } + } } - // Build new DTO structure - - // Calculate totals - var totalPopulation float64 - for _, chickin := range chickins { - totalPopulation += chickin.UsageQty + costs.ExpeditionCost, err = s.HppRepo.GetExpedisionCost(c.Context(), projectFlockKandangIDs) + if err != nil { + costs.ExpeditionCost = 0 } - // Calculate actual population (total population - depletion) - actualPopulation := totalPopulation - totalDepletion - - // Calculate budget totals by category - calculateBudgetByFlag := func(flags []string) float64 { - var total float64 + if budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlock.Id); err == nil { + totalBudget := 0.0 for _, budget := range budgets { - if budget.Nonstock != nil { - for _, nonstockFlag := range budget.Nonstock.Flags { - flagName := strings.ToUpper(nonstockFlag.Name) - for _, targetFlag := range flags { - if flagName == strings.ToUpper(targetFlag) { - total += budget.Price * budget.Qty - break - } - } - } + totalBudget += budget.Price * budget.Qty + } + if projectFlockKandangID != nil { + allKandangs, errKandang := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) + if errKandang == nil && len(allKandangs) > 0 { + costs.BudgetOperational = totalBudget / float64(len(allKandangs)) + } + } else { + costs.BudgetOperational = totalBudget + } + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch budgets for project_flock_id=%d: %+v", projectFlock.Id, err) + } + + if realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID); err == nil { + for _, realization := range realizations { + amount := realization.Price * realization.Qty + isEkspedisi := realization.ExpenseNonstock != nil && + realization.ExpenseNonstock.Nonstock != nil && + containsFlag(realization.ExpenseNonstock.Nonstock.Flags, "EKSPEDISI") + if !isEkspedisi { + costs.RealizationOperational += amount } } - return total + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch realizations for project_flock_id=%d: %+v", projectFlock.Id, err) } - // Budget per category - budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}) - budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"}) - budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"}) - budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"}) + return costs, nil +} - // Operational budget = total budget - pakan - ovk - ayam - ekspedisi - totalBudgetAmount := 0.0 - for _, budget := range budgets { - totalBudgetAmount += budget.Price * budget.Qty +func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*ProductionData, error) { + data := &ProductionData{} + var err error + + data.TotalPopulationIn, err = s.HppRepo.GetTotalPopulation(c.Context(), projectFlockKandangIDs) + if err != nil { + data.TotalPopulationIn = 0 } - budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi + if projectFlockKandangID != nil { + data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) + } else { + data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) + } + if err != nil { + data.TotalDepletion = 0 + } - // Calculate realization totals - var totalRealizationAmount float64 - var totalEkspedisiRealization float64 - for _, realization := range realizations { - amount := realization.Price * realization.Qty - totalRealizationAmount += amount + if projectFlockKandangID != nil { + data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) + } else { + data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) + } + if err != nil { + data.TotalWeightProduced = 0 + } - // Check if this is ekspedisi (need to check nonstock flags) - if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil { - for _, flag := range realization.ExpenseNonstock.Nonstock.Flags { - if flag.Name == "EKSPEDISI" { - totalEkspedisiRealization += amount - break - } - } + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil) + if err != nil { + data.TotalEggWeightKg = 0 } } - totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization + var deliveryProducts []entity.MarketingDeliveryProduct + if projectFlockKandangID != nil { + deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category) + } else { + deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, nil, projectFlock.Category) + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan") + } - // Filter delivery products based on category - var filteredDeliveryProducts []entity.MarketingDeliveryProduct for _, delivery := range deliveryProducts { - // Get product from delivery if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { continue } - - product := delivery.MarketingProduct.ProductWarehouse.Product - isEggProduct := false - isChickenProduct := false - - // Check product flags - for _, flag := range product.Flags { - flagName := strings.ToUpper(flag.Name) - - // Egg product flags - if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" || - flagName == "TELURPUTIH" || flagName == "TELURRETAK" { - isEggProduct = true - } - - // Chicken product flags - if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" { - isChickenProduct = true - } - } - - // Filter based on project flock category - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - // Laying: only egg products - if isEggProduct { - filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) - } - } else { - // Growing/Contract Growing: only chicken products - if isChickenProduct || (!isEggProduct && !isChickenProduct) { - // Include if chicken product or if no specific flags (default to chicken) - filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) - } - } + data.TotalWeightSold += delivery.TotalWeight + data.TotalSalesAmount += delivery.TotalPrice } + return data, nil +} - // Calculate total weight sold and sales amount from filtered products - var totalWeightSold float64 - var totalSalesAmount float64 - for _, delivery := range filteredDeliveryProducts { - totalWeightSold += delivery.TotalWeight - totalSalesAmount += delivery.TotalPrice +func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection { + + actualPopulation := production.TotalPopulationIn - production.TotalDepletion + totalWeightProduced := production.TotalWeightProduced + totalEggWeightKg := production.TotalEggWeightKg + + weightForCalculation := totalWeightProduced + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + weightForCalculation = totalEggWeightKg } - - // Calculate metrics - always use kg ayam for rp_per_kg calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { - rpPerBird = amount / actualPopulation // Use actual population + rpPerBird = amount / actualPopulation } - if totalWeightProduced > 0 { - rpPerKg = amount / totalWeightProduced + if weightForCalculation > 0 { + rpPerKg = amount / weightForCalculation } return } - // Calculate metrics for profit loss (use total population and total weight produced) - calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { - if totalPopulation > 0 { - rpPerBird = amount / totalPopulation - } - if totalWeightProduced > 0 { - rpPerKg = amount / totalWeightProduced - } - return + createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem { + budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount) + realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount) + return dto.ToHPPItem( + id, + category, + code, + label, + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ) } - // Build HPP Items using constants hppItems := []dto.HPPItem{} - // PAKAN item - pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan) - pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 1, - "purchase", - string(dto.HPPCodePakan), - "Pembelian Pakan", - dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan), - dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice), - )) + hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost)) + hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost)) - // OVK item - ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk) - ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 2, - "purchase", - string(dto.HPPCodeOVK), - "Pembelian OVK", - dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk), - dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice), - )) - - // DOC/DEPRESIASI item docCode := string(dto.HPPCodeDOC) docLabel := "Pembelian DOC" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { docCode = string(dto.HPPCodeDepresiasi) docLabel = "Depresiasi" } - docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam) - docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 3, - "purchase", - docCode, - docLabel, - dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam), - dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice), - )) + hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost)) + hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational)) + hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost)) - // OVERHEAD item - overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational) - overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization) - hppItems = append(hppItems, dto.ToHPPItem( - 4, - "overhead", - string(dto.HPPCodeOverhead), - "Pengeluaran Overhead", - dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational), - dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization), - )) - - // EKSPEDISI item - ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi) - ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization) - hppItems = append(hppItems, dto.ToHPPItem( - 5, - "overhead", - string(dto.HPPCodeEkspedisi), - "Beban Ekspedisi", - dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi), - dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization), - )) - - // HPP Summary - totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi - totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization + totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost + totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp) hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp) var eggBudgeting, eggRealization *dto.FinancialMetrics - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 { - eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg - eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg - eggBudgeting = &dto.FinancialMetrics{ - RpPerBird: 0, - RpPerKg: eggBudgetRpPerKg, - Amount: totalBudgetHpp, + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) { + if *metrics == nil { + *metrics = &dto.FinancialMetrics{ + RpPerBird: 0, + RpPerKg: rpPerKg, + Amount: amount, + } + } else { + (*metrics).Amount += amount + if totalEggWeightKg > 0 { + (*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg + } + } } - eggRealization = &dto.FinancialMetrics{ - RpPerBird: 0, - RpPerKg: eggRealizationRpPerKg, - Amount: totalRealizationHpp, + + for _, projectFlockKandang := range projectFlockKandangs { + hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil) + if err == nil { + accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg) + accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg) + } } } @@ -543,12 +378,48 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl eggRealization, ) - hppSection := dto.ToHPPSection(hppItems, hppSummary) + return dto.ToHPPSection(hppItems, hppSummary) +} + +func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { + + totalPopulationIn := production.TotalPopulationIn + totalWeightProduced := production.TotalWeightProduced + totalEggWeightKg := production.TotalEggWeightKg + totalSalesAmount := production.TotalSalesAmount + totalWeightSold := production.TotalWeightSold + + weightForSales := totalWeightSold + weightForCalculation := totalWeightProduced + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + weightForSales = totalWeightSold + weightForCalculation = totalEggWeightKg + } + + calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if totalPopulationIn > 0 { + rpPerBird = amount / totalPopulationIn + } + if weightForSales > 0 { + rpPerKg = amount / weightForSales + } + return + } + + actualPopulation := production.TotalPopulationIn - production.TotalDepletion + + calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if weightForCalculation > 0 { + rpPerKg = amount / weightForCalculation + } + return + } - // Build Profit Loss Items using constants plItems := []dto.ProfitLossItem{} - // SALES item salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { @@ -563,10 +434,13 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl totalSalesAmount, )) - // SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK - totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice - sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird - sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg + totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost + _, sapronakRpPerKg := calculateMetrics(totalSapronakAmount) + sapronakRpPerBird := 0.0 + for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { + rpPerBird, _ := calculateMetrics(amount) + sapronakRpPerBird += rpPerBird + } sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), @@ -577,62 +451,54 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl totalSapronakAmount, )) - // OVERHEAD item - overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization) + overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", "overhead", overheadRpPerBird, overheadRpPerKg, - totalOperationalRealization, + costs.RealizationOperational, )) - // EKSPEDISI item + ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", "overhead", - ekspedisiRealizationRpPerBird, - ekspedisiRealizationRpPerKg, - totalEkspedisiRealization, + ekspedisiRpPerBird, + ekspedisiRpPerKg, + costs.ExpeditionCost, )) - // Profit Loss Summary - // Gross Profit = Sales - (DOC + PAKAN + OVK) only - // Gross Profit should NOT include overhead and ekspedisi - costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice + costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost costOfGoodsSoldRpPerBird := sapronakRpPerBird + costOfGoodsSoldRpPerKg := sapronakRpPerKg grossProfit := totalSalesAmount - costOfGoodsSold grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird + grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg - // Operating Expenses (Overhead + Ekspedisi) - totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization - totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird + totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost + totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird + totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg - // Net Profit = Gross Profit - Operating Expenses netProfit := grossProfit - totalOperatingExpenses netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird + netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg plSummary := dto.ToProfitLossSummary( - dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit), - dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses), - dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit), + dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit), + dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses), + dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit), ) - profitLossSection := dto.ToProfitLossSection(plItems, plSummary) - - // Build complete response - data := dto.ToClosingKeuanganData(hppSection, profitLossSection) - - return &data, nil + return dto.ToProfitLossSection(plItems, plSummary) } -// containsItem checks if a string exists in a slice -func containsItem(slice []string, item string) bool { - for _, s := range slice { - if strings.EqualFold(s, item) { +func containsFlag(flags []entity.Flag, name string) bool { + for _, flag := range flags { + if flag.Name == name { return true } } diff --git a/internal/modules/inventory/adjustments/dto/adjustment.dto.go b/internal/modules/inventory/adjustments/dto/adjustment.dto.go index 1ce3da1b..c07f84f9 100644 --- a/internal/modules/inventory/adjustments/dto/adjustment.dto.go +++ b/internal/modules/inventory/adjustments/dto/adjustment.dto.go @@ -103,7 +103,7 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO { func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO { return AdjustmentRelationDTO{ Id: e.Id, - Note: e.StockLog.Notes, + Note: "", Increase: e.TotalQty, Decrease: e.UsageQty, ProductWarehouseId: e.ProductWarehouseId, @@ -113,24 +113,17 @@ func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO { func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO { var createdUser *userDTO.UserRelationDTO - if e.StockLog != nil && e.StockLog.CreatedUser != nil { - createdUser = &userDTO.UserRelationDTO{ - Id: e.StockLog.CreatedUser.Id, - IdUser: e.StockLog.CreatedUser.IdUser, - Email: e.StockLog.CreatedUser.Email, - Name: e.StockLog.CreatedUser.Name, - } - } - createdAt := time.Time{} - if e.StockLog != nil { - createdAt = e.StockLog.CreatedAt + // Get created user from StockLog + if e.StockLog != nil && e.StockLog.CreatedUser != nil { + mapped := userDTO.ToUserRelationDTO(*e.StockLog.CreatedUser) + createdUser = &mapped } return AdjustmentListDTO{ AdjustmentRelationDTO: ToAdjustmentRelationDTO(e), CreatedUser: createdUser, - CreatedAt: createdAt, + CreatedAt: e.CreatedAt, } } diff --git a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go index fa2685e7..f62738a3 100644 --- a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go +++ b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go @@ -9,7 +9,7 @@ import ( type AdjustmentStockRepository interface { CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error - GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) + GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) WithTx(tx *gorm.DB) AdjustmentStockRepository DB() *gorm.DB } @@ -30,19 +30,13 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent return q.Create(data).Error } -func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) { +func (r *adjustmentStockRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) { var record entity.AdjustmentStock - err := r.db.WithContext(ctx). - Preload("StockLog"). - Preload("StockLog.ProductWarehouse"). - Preload("StockLog.ProductWarehouse.Product"). - Preload("StockLog.ProductWarehouse.Warehouse"). - Preload("StockLog.CreatedUser"). - Preload("ProductWarehouse"). - Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse"). - Where("stock_log_id = ?", stockLogID). - First(&record).Error + q := r.db.WithContext(ctx) + if modifier != nil { + q = modifier(q) + } + err := q.First(&record, id).Error if err != nil { return nil, err } diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index bec0ef74..16bcf70a 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -70,11 +70,11 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). - Preload("CreatedUser") + Preload("StockLog.CreatedUser") } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) { - adjustmentStock, err := s.AdjustmentStockRepository.GetByStockLogID(c.Context(), id) + adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") @@ -164,13 +164,13 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e if transactionType == string(utils.StockLogTransactionTypeIncrease) { afterQuantity += req.Quantity - newLog.Increase = afterQuantity + newLog.Increase = req.Quantity } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) } afterQuantity -= req.Quantity - newLog.Decrease = afterQuantity + newLog.Decrease = req.Quantity } if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { @@ -179,7 +179,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } adjustmentStock := &entity.AdjustmentStock{ - StockLogId: newLog.Id, ProductWarehouseId: productWarehouse.Id, } if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { @@ -187,6 +186,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") } + newLog.LoggableType = string(utils.StockLogTypeAdjustment) + newLog.LoggableId = adjustmentStock.Id + if err := s.StockLogsRepository.WithTx(tx).UpdateOne(ctx, newLog.Id, newLog, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to link stock log") + } + if transactionType == string(utils.StockLogTransactionTypeIncrease) { note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) @@ -216,7 +221,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } } - // LEGACY: Update ProductWarehouse quantity (for backward compatibility/reporting) productWarehouse.Quantity = afterQuantity if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) @@ -295,29 +299,23 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu var total int64 q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}). - Preload("StockLog"). - Preload("StockLog.ProductWarehouse"). - Preload("StockLog.ProductWarehouse.Product"). - Preload("StockLog.ProductWarehouse.Warehouse"). - Preload("StockLog.CreatedUser"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Warehouse"). + Preload("StockLog.CreatedUser") if query.ProductID > 0 { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). - Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.product_id = ?", query.ProductID) } if query.WarehouseID > 0 { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). - Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.warehouse_id = ?", query.WarehouseID) } if query.TransactionType != "" { - q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). + q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT"). Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType)) } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index ea1041ea..fe5f8f5a 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -235,6 +235,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx) stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx) productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx) + stocklogsRepoTx := s.StockLogsRepository.WithTx(tx) if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil { return err @@ -405,6 +406,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking") } + stockLogDecrease := &entity.StockLog{ + ProductWarehouseId: uint(*detail.SourceProductWarehouseID), + CreatedBy: uint(actorID), + Increase: 0, + Decrease: product.ProductQty, + LoggableType: string(utils.StockLogTypeTransfer), + LoggableId: uint(detail.Id), + Notes: "", + } + if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar") + } + note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyStockTransferIn, @@ -427,6 +441,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err) return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking") } + + stockLogIncrease := &entity.StockLog{ + ProductWarehouseId: uint(*detail.DestProductWarehouseID), + CreatedBy: uint(actorID), + Increase: product.ProductQty, + Decrease: 0, + LoggableType: string(utils.StockLogTypeTransfer), + LoggableId: uint(detail.Id), + Notes: "", + } + if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk") + } } if len(req.Deliveries) > 0 { diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 1ec0bddf..bcd788cd 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -14,7 +14,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) - GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) + GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) @@ -54,12 +54,14 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo return deliveryProducts, nil } -func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { +func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct db := r.DB().WithContext(ctx). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Where("marketing_delivery_products.delivery_date IS NOT NULL"). @@ -69,6 +71,25 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID) } + if category == string(utils.ProjectFlockCategoryLaying) { + db = db.Where("flags.name IN (?)", []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + }) + } else { + db = db.Where("flags.name IN (?)", []string{ + string(utils.FlagDOC), + string(utils.FlagPullet), + string(utils.FlagLayer), + string(utils.FlagAyamAfkir), + string(utils.FlagAyamCulling), + string(utils.FlagAyamMati), + }) + } + db = db. Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). diff --git a/internal/modules/production/project_flocks/repositories/project_budget.repository.go b/internal/modules/production/project_flocks/repositories/project_budget.repository.go index 720bfc40..06869795 100644 --- a/internal/modules/production/project_flocks/repositories/project_budget.repository.go +++ b/internal/modules/production/project_flocks/repositories/project_budget.repository.go @@ -31,6 +31,7 @@ func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, p Where("project_flock_id = ?", projectFlockID). Preload("Nonstock"). Preload("Nonstock.Uom"). + Preload("Nonstock.Flags"). Find(&budgets).Error return budgets, err } diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 474a53c2..f5b55a78 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -85,6 +85,7 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont var records []entity.ProjectFlockKandang if err := r.db.WithContext(ctx). Where("project_flock_id = ?", projectFlockID). + Preload("Kandang"). Find(&records).Error; err != nil { return nil, err } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 6cb65c6c..27c399f4 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -47,8 +47,10 @@ type RecordingRepository interface { GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) + GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) + GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (totalDepletion float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) @@ -473,6 +475,17 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context. return result, err } +func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_depletions"). + Select("COALESCE(SUM(recording_depletions.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). + Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangID). + Scan(&result).Error + return result, err +} + func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { // Body-weight tracking is removed; keep stub for report compatibility. return 0, nil @@ -609,3 +622,23 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF return result.TotalWeight, err } + +func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + if projectFlockKandangID == 0 { + return 0, nil + } + + var result struct { + TotalWeight float64 + } + + err := r.DB().WithContext(ctx). + Table("project_flock_kandang_uniformity"). + Select("COALESCE((mean_up / 1.10) * chick_qty_of_weight / 1000, 0) as total_weight"). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Order("id DESC"). + Limit(1). + Scan(&result).Error + + return result.TotalWeight, err +} diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index d0ee5061..581b9093 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -9,6 +9,7 @@ import ( service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -28,9 +29,11 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), - TransferDate: c.Query("transfer_date", ""), - FlockSource: uint(c.QueryInt("flock_source", 0)), - FlockDestination: uint(c.QueryInt("flock_destination", 0)), + StartDate: c.Query("start_date", ""), + EndDate: c.Query("end_date", ""), + FlockSource: utils.ParseQueryUintArray(c.Query("flock_source", "")), + FlockDestination: utils.ParseQueryUintArray(c.Query("flock_destination", "")), + Status: utils.ParseQueryArray(c.Query("status", "")), } if query.Page < 1 || query.Limit < 1 { @@ -218,3 +221,29 @@ func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error Data: resp, }) } + +func (u *TransferLayingController) GetMaxTargetQtyPerKandang(c *fiber.Ctx) error { + projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + kandangMaxTargetQty, err := u.TransferLayingService.GetMaxTargetQtyPerKandang(c, uint(projectFlockID)) + if err != nil { + return err + } + + kandangs := make([]dto.KandangMaxTargetQtyDTO, 0, len(kandangMaxTargetQty)) + for pfkId, maxTargetQty := range kandangMaxTargetQty { + kandangs = append(kandangs, dto.ToKandangMaxTargetQtyDTO(pfkId, maxTargetQty)) + } + + resp := dto.ToMaxTargetQtyForTransferDTO(uint(projectFlockID), kandangs) + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get max target quantity successfully", + Data: resp, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index dfc5e5d9..53e069b2 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -5,6 +5,9 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -17,60 +20,35 @@ type TransferLayingRelationDTO struct { Notes string `json:"notes"` } -type ProjectFlockSummaryDTO struct { - Id uint `json:"id"` - FlockName string `json:"flock_name"` - Category string `json:"category"` -} - -type ProductSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type WarehouseSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - -type ProductWarehouseSummaryDTO struct { - Product *ProductSummaryDTO `json:"product,omitempty"` - Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"` -} - -type ProjectFlockKandangSummaryDTO struct { - Id uint `json:"id"` - Kandang *KandangSummaryDTO `json:"kandang,omitempty"` -} - -type KandangSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` +type ProjectFlockKandangWithKandangDTO struct { + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` } type LayingTransferSourceDTO struct { - SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"` - Qty float64 `json:"qty"` - ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` - Note string `json:"note,omitempty"` + SourceProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"source_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type LayingTransferTargetDTO struct { - TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"` - Qty float64 `json:"qty"` - ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` - Note string `json:"note,omitempty"` + TargetProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"target_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type TransferLayingListDTO struct { TransferLayingRelationDTO - FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` - ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` - CreatedBy uint `json:"created_by"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` + FromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"from_project_flock,omitempty"` + ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` } type TransferLayingDetailDTO struct { @@ -94,70 +72,26 @@ type AvailableQtyForTransferDTO struct { Kandangs []KandangAvailableQtyDTO `json:"kandangs"` } +// === Max Target Quantity DTOs === + +type KandangMaxTargetQtyDTO struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + MaxTargetQty float64 `json:"max_target_qty"` +} + +type MaxTargetQtyForTransferDTO struct { + ProjectFlockId uint `json:"project_flock_id"` + ProjectFlockKandangs []KandangMaxTargetQtyDTO `json:"project_flock_kandangs"` +} + // === Mapper Functions === -func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { - if pf == nil || pf.Id == 0 { - return nil - } - - return &ProjectFlockSummaryDTO{ - Id: pf.Id, - FlockName: pf.FlockName, - Category: pf.Category, - } -} - -func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO { - if pfk == nil || pfk.Id == 0 { - return nil - } - - var kandang *KandangSummaryDTO - if pfk.Kandang.Id != 0 { - kandang = &KandangSummaryDTO{ - Id: pfk.Kandang.Id, - Name: pfk.Kandang.Name, - } - } - - return &ProjectFlockKandangSummaryDTO{ - Id: pfk.Id, - Kandang: kandang, - } -} - -func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO { - if product == nil || product.Id == 0 { - return nil - } - - return &ProductSummaryDTO{ - Id: product.Id, - Name: product.Name, - } -} - -func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO { - if warehouse == nil || warehouse.Id == 0 { - return nil - } - - return &WarehouseSummaryDTO{ - Id: warehouse.Id, - Name: warehouse.Name, - Type: warehouse.Type, - } -} - -func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO { - if pw == nil || pw.Id == 0 { - return nil - } - - return &ProductWarehouseSummaryDTO{ - Product: ToProductSummaryDTO(&pw.Product), - Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse), +func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { + return TransferLayingRelationDTO{ + Id: e.Id, + TransferNumber: e.TransferNumber, + TransferDate: e.TransferDate, + Notes: e.Notes, } } @@ -172,10 +106,29 @@ func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransfe displayQty = source.RequestedQty } + var pfkDTO *ProjectFlockKandangWithKandangDTO + if source.SourceProjectFlockKandang != nil && source.SourceProjectFlockKandang.Id != 0 { + pfkDTO = &ProjectFlockKandangWithKandangDTO{ + Id: source.SourceProjectFlockKandang.Id, + KandangId: source.SourceProjectFlockKandang.KandangId, + ProjectFlockId: source.SourceProjectFlockKandang.ProjectFlockId, + } + if source.SourceProjectFlockKandang.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(source.SourceProjectFlockKandang.Kandang) + pfkDTO.Kandang = &mapped + } + } + + var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO + if source.ProductWarehouse != nil && source.ProductWarehouse.Id != 0 { + mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*source.ProductWarehouse) + pwDTO = &mapped + } + return LayingTransferSourceDTO{ - SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), + SourceProjectFlockKandang: pfkDTO, Qty: displayQty, - ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), + ProductWarehouse: pwDTO, Note: source.Note, } } @@ -192,10 +145,29 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT } func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { + var pfkDTO *ProjectFlockKandangWithKandangDTO + if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 { + pfkDTO = &ProjectFlockKandangWithKandangDTO{ + Id: target.TargetProjectFlockKandang.Id, + KandangId: target.TargetProjectFlockKandang.KandangId, + ProjectFlockId: target.TargetProjectFlockKandang.ProjectFlockId, + } + if target.TargetProjectFlockKandang.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangRelationDTO(target.TargetProjectFlockKandang.Kandang) + pfkDTO.Kandang = &mapped + } + } + + var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO + if target.ProductWarehouse != nil && target.ProductWarehouse.Id != 0 { + mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*target.ProductWarehouse) + pwDTO = &mapped + } + return LayingTransferTargetDTO{ - TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), - Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity) - ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), + TargetProjectFlockKandang: pfkDTO, + Qty: target.TotalQty, + ProductWarehouse: pwDTO, Note: target.Note, } } @@ -211,15 +183,6 @@ func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingT return result } -func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { - return TransferLayingRelationDTO{ - Id: e.Id, - TransferNumber: e.TransferNumber, - TransferDate: e.TransferDate, - Notes: e.Notes, - } -} - func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { @@ -227,26 +190,52 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { createdUser = &mapped } + var approval *approvalDTO.ApprovalRelationDTO + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + approval = &mapped + } + + // Build from project flock DTO + var fromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO + if e.FromProjectFlock != nil && e.FromProjectFlock.Id != 0 { + fromProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{ + Id: e.FromProjectFlock.Id, + FlockName: e.FromProjectFlock.FlockName, + } + } + + var toProjectFlock *projectFlockDTO.ProjectFlockRelationDTO + if e.ToProjectFlock != nil && e.ToProjectFlock.Id != 0 { + toProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{ + Id: e.ToProjectFlock.Id, + FlockName: e.ToProjectFlock.FlockName, + } + } + return TransferLayingListDTO{ TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), - FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), - ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), + FromProjectFlock: fromProjectFlock, + ToProjectFlock: toProjectFlock, CreatedBy: e.CreatedBy, CreatedUser: createdUser, CreatedAt: e.CreatedAt, + Approval: approval, } } func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { var latestApproval *approvalDTO.ApprovalRelationDTO - if e.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + // Prioritas: e.LatestApproval > approvals slice + approvalToMap := e.LatestApproval + if approvalToMap == nil && len(approvals) > 0 { + approvalToMap = &approvals[len(approvals)-1] + } + + if approvalToMap != nil { + mapped := approvalDTO.ToApprovalDTO(*approvalToMap) latestApproval = &mapped - } else if len(approvals) > 0 { - // Fallback to approvals slice - latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1]) - latestApproval = &latest } return TransferLayingDetailDTO{ @@ -260,13 +249,14 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { var mappedApproval *approvalDTO.ApprovalRelationDTO - // Prefer LatestApproval from entity - if e.LatestApproval != nil && e.LatestApproval.Id != 0 { - mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) - mappedApproval = &mapped - } else if approval != nil && approval.Id != 0 { - // Fallback to passed approval parameter - mapped := approvalDTO.ToApprovalDTO(*approval) + // Prioritas: e.LatestApproval > approval parameter + approvalToMap := e.LatestApproval + if approvalToMap == nil && approval != nil { + approvalToMap = approval + } + + if approvalToMap != nil { + mapped := approvalDTO.ToApprovalDTO(*approvalToMap) mappedApproval = &mapped } @@ -285,3 +275,17 @@ func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingLis } return result } + +func ToKandangMaxTargetQtyDTO(pfkId uint, maxTargetQTY float64) KandangMaxTargetQtyDTO { + return KandangMaxTargetQtyDTO{ + ProjectFlockKandangId: uint(pfkId), + MaxTargetQty: maxTargetQTY, + } +} + +func ToMaxTargetQtyForTransferDTO(pfId uint, kandangs []KandangMaxTargetQtyDTO) MaxTargetQtyForTransferDTO { + return MaxTargetQtyForTransferDTO{ + ProjectFlockId: pfId, + ProjectFlockKandangs: kandangs, + } +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 3dab5120..14fa4118 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -12,6 +13,9 @@ type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) IdExists(ctx context.Context, id uint) (bool, error) + + // Tambah method baru untuk query dengan filter lengkap + GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) } type TransferLayingRepositoryImpl struct { @@ -40,3 +44,93 @@ func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, } return &transfer, nil } + +type GetAllFilterParams struct { + Search string + StartDate string + EndDate string + FlockSource []uint + FlockDestination []uint + Status []string +} + +func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) { + var records []entity.LayingTransfer + var total int64 + + q := r.db.WithContext(ctx).Model(&entity.LayingTransfer{}) + + if params.Search != "" { + searchPattern := "%" + params.Search + "%" + q = q.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). + Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). + Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) + } + + if params.StartDate != "" && params.EndDate != "" { + q = q.Where("transfer_date::date >= ?::date AND transfer_date::date <= ?::date", + params.StartDate, params.EndDate) + } else if params.StartDate != "" { + q = q.Where("transfer_date::date >= ?::date", params.StartDate) + } else if params.EndDate != "" { + q = q.Where("transfer_date::date <= ?::date", params.EndDate) + } + + if len(params.FlockSource) > 0 { + q = q.Where("from_project_flock_id IN ?", params.FlockSource) + } + + if len(params.FlockDestination) > 0 { + q = q.Where("to_project_flock_id IN ?", params.FlockDestination) + } + + if len(params.Status) > 0 { + statusConditions := []string{} + statusValues := []interface{}{} + + for _, status := range params.Status { + switch status { + case "PENDING": + statusConditions = append(statusConditions, + "NOT EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id)") + + case "APPROVED": + statusConditions = append(statusConditions, + "EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'APPROVED' ORDER BY created_at DESC LIMIT 1)") + + case "REJECTED": + statusConditions = append(statusConditions, + "EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'REJECTED' ORDER BY created_at DESC LIMIT 1)") + } + } + + if len(statusConditions) > 0 { + q = q.Where("("+strings.Join(statusConditions, " OR ")+")", statusValues...) + } + } + + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + + q = q.Offset(offset).Limit(limit). + Preload("FromProjectFlock"). + Preload("ToProjectFlock"). + Preload("CreatedUser"). + Preload("Sources"). + Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.SourceProjectFlockKandang.Kandang"). + Preload("Sources.ProductWarehouse"). + Preload("Targets"). + Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.TargetProjectFlockKandang.Kandang"). + Preload("Targets.ProductWarehouse"). + Order("laying_transfers.created_at DESC") + + if err := q.Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 8f7a62c0..c16ba1a8 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -21,11 +21,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Post("/approval", m.Auth(u), ctrl.Approval) - route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) - route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) - route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) - route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) - route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) - route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) + route.Get("/", m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) + route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) + route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) + route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) + route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 8e0269cf..a5d0ba88 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -34,6 +34,7 @@ type TransferLayingService interface { DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) + GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) } type transferLayingService struct { @@ -108,34 +109,20 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ offset := (params.Page - 1) * params.Limit - transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - // Apply search and filters - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). - Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). - Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", - searchPattern, searchPattern, searchPattern, searchPattern) - } + filterParams := &repository.GetAllFilterParams{ + Search: params.Search, + StartDate: params.StartDate, + EndDate: params.EndDate, + FlockSource: params.FlockSource, + FlockDestination: params.FlockDestination, + Status: params.Status, + } - if params.TransferDate != "" { - db = db.Where("transfer_date::date = ?::date", params.TransferDate) - } - - if params.FlockSource > 0 { - db = db.Where("from_project_flock_id = ?", params.FlockSource) - } - - if params.FlockDestination > 0 { - db = db.Where("to_project_flock_id = ?", params.FlockDestination) - } - - db = db.Order("created_at DESC") - - db = s.withRelations(db) - - return db - }) + transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams) + if err != nil { + s.Log.Errorf("Failed to get transferLayings: %+v", err) + return nil, 0, err + } if err != nil { s.Log.Errorf("Failed to get transferLayings: %+v", err) @@ -888,3 +875,39 @@ func (s *transferLayingService) validateKandangOwnership( return nil } + +func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) { + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { + return nil, err + } + + projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + return nil, err + } + + kandangMaxTargetQty := make(map[uint]float64) + for _, projectFlockKandang := range projectFlockKandangs { + + capacity := projectFlockKandang.Kandang.Capacity + + availableQty, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID( + c.Context(), + projectFlockKandang.Id, + ) + if err != nil { + return nil, err + } + + kandangMaxTargetQty[projectFlockKandang.Id] = capacity - availableQty + + if kandangMaxTargetQty[projectFlockKandang.Id] < 0 { + kandangMaxTargetQty[projectFlockKandang.Id] = 0 + } + } + + return kandangMaxTargetQty, nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 06d52316..0472ba39 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -29,12 +29,14 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty"` - TransferDate string `query:"transfer_date" validate:"omitempty"` - FlockSource uint `query:"flock_source" validate:"omitempty,number"` - FlockDestination uint `query:"flock_destination" validate:"omitempty,number"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` + FlockSource []uint `query:"flock_source" validate:"omitempty"` + FlockDestination []uint `query:"flock_destination" validate:"omitempty"` + Status []string `query:"status" validate:"omitempty"` } type Approve struct { diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 1210b3a1..d3bf2bbf 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -45,10 +45,7 @@ type groupedItem struct { projectFK *uint kandangID *uint totalPrice float64 -} - -func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { - return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID) + poNumber string } type expenseBridge struct { @@ -222,6 +219,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB { return db. Preload("Items"). + Preload("Items.Product"). Preload("Items.Warehouse"). Preload("Items.Warehouse.Kandang") }) @@ -309,7 +307,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ // If supplier/date unchanged, update nonstock in place. if oldSupplier == supplierID && oldDate.Equal(newDate) { - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) if err := b.db.WithContext(ctx). Model(&entity.ExpenseNonstock{}). Where("id = ?", link.ExpenseNonstockID). @@ -340,7 +338,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ if err != nil { return err } - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) if err := b.db.WithContext(ctx). Model(&entity.Expense{}). Where("id = ?", link.ExpenseID). @@ -392,6 +390,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ projectFK: projectFK, kandangID: kandangID, totalPrice: totalPrice, + poNumber: purchasePoNumber(purchase), } newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID) @@ -410,7 +409,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ createdNonstockID = noteMap[payload.PurchaseItemID] } - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) updateBody := map[string]interface{}{ "expense_id": expenseDetail.Id, "qty": payload.ReceivedQty, @@ -483,6 +482,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ projectFK: projectFK, kandangID: kandangID, totalPrice: totalPrice, + poNumber: purchasePoNumber(purchase), }) } @@ -679,6 +679,14 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail Update("expense_nonstock_id", expenseNonstockID).Error; err != nil { return err } + + note := purchaseItemDisplayNote(gi.item, gi.payload.PurchaseItemID, gi.poNumber) + if err := b.db.WithContext(ctx). + Model(&entity.ExpenseNonstock{}). + Where("id = ?", expenseNonstockID). + Update("notes", note).Error; err != nil { + return err + } } return nil @@ -709,3 +717,22 @@ func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 { } return result } + +func purchaseItemDisplayNote(item *entity.PurchaseItem, itemID uint, poNumber string) string { + poLabel := "PO" + if strings.TrimSpace(poNumber) != "" { + poLabel = strings.TrimSpace(poNumber) + } + productName := fmt.Sprintf("Item %d", itemID) + if item != nil && item.Product != nil && strings.TrimSpace(item.Product.Name) != "" { + productName = item.Product.Name + } + return fmt.Sprintf("%s (%s)", poLabel, productName) +} + +func purchasePoNumber(purchase *entity.Purchase) string { + if purchase == nil || purchase.PoNumber == nil { + return "" + } + return *purchase.PoNumber +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 9c5b8faf..fb5d72ba 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1617,7 +1617,7 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes var avgWeight float64 eggHpp := 0.0 if s.HppSvc != nil { - hppCost, err := s.HppSvc.CalculateHppCost(row.ProjectFlockKandangID, &endOfDay) + hppCost, err := s.HppSvc.CalculateHppCost(row.ProjectFlockKandangID, &periodDate) if err != nil { return nil, nil, err } @@ -1626,7 +1626,9 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes eggHpp = hppCost.Estimation.HargaKg eggTotalPiecesFloat = hppCost.Estimation.Butir eggWeightFloat = hppCost.Estimation.Kg - avgWeight = eggWeightFloat / eggTotalPiecesFloat + if eggTotalPiecesFloat > 0 { + avgWeight = eggWeightFloat / eggTotalPiecesFloat + } eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining } } @@ -1642,6 +1644,9 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) { eggWeightFloat = 0 } + if math.IsNaN(avgWeight) || math.IsInf(avgWeight, 0) { + avgWeight = 0 + } if params.WeightMin != nil && avgWeight < *params.WeightMin { continue diff --git a/internal/utils/strings.go b/internal/utils/strings.go index a58ba1ac..e9e23f84 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -2,6 +2,7 @@ package utils import ( "sort" + "strconv" "strings" ) @@ -47,3 +48,54 @@ func ParseFlags(raw string) []string { sort.Strings(res) return res } + +// ParseQueryArray parses comma-separated string values and returns a slice of trimmed strings +// Example: "a, b, c" → ["a", "b", "c"] +func ParseQueryArray(raw string) []string { + if raw == "" { + return nil + } + + parts := strings.Split(raw, ",") + result := make([]string, 0, len(parts)) + + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + + if len(result) == 0 { + return nil + } + return result +} + +// ParseQueryUintArray parses comma-separated string values and returns a slice of uint +// Invalid values are skipped +// Example: "1, 2, 3" → [1, 2, 3] +func ParseQueryUintArray(raw string) []uint { + if raw == "" { + return nil + } + + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed == "" { + continue + } + + if num, err := strconv.ParseUint(trimmed, 10, 32); err == nil { + result = append(result, uint(num)) + } + } + + if len(result) == 0 { + return nil + } + return result +}