diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index d380dc3d..cf4b5b54 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,5 +1,12 @@ package dto +import ( + "strings" + + "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + // === BASE METRICS === type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` @@ -28,9 +35,7 @@ type SummaryHpp struct { Comparison } -// Ini adalah struct mandiri untuk bagian HPP Purchases type HppPurchasesSection struct { - Title string `json:"title"` Hpp []HppGroup `json:"hpp"` SummaryHpp SummaryHpp `json:"summary_hpp"` } @@ -58,14 +63,11 @@ type ProfitLossData struct { Summary PLSummaryGroup `json:"summary"` } -// Ini adalah struct mandiri untuk bagian Profit Loss type ProfitLossSection struct { - Title string `json:"title"` - Data ProfitLossData `json:"data"` + Data ProfitLossData `json:"data"` } // === RESPONSE DTO (ROOT) === -// Sekarang Root-nya terlihat sangat bersih dan tidak "janggal" lagi type ReportResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"` ProfitLoss ProfitLossSection `json:"profit_loss"` @@ -73,7 +75,6 @@ type ReportResponse struct { // === MAPPER FUNCTIONS === -// FinancialMetrics Mappers func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { return FinancialMetrics{ RpPerBird: rpPerBird, @@ -82,7 +83,6 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { } } -// Comparison Mappers func ToComparison(budgeting, realization FinancialMetrics) Comparison { return Comparison{ Budgeting: budgeting, @@ -90,40 +90,141 @@ func ToComparison(budgeting, realization FinancialMetrics) Comparison { } } -// HppItem Mappers -func ToHppItem(itemType string, comparison Comparison) HppItem { - return HppItem{ - Type: itemType, - Comparison: comparison, - } +// === HPP PENGELUARAN (from Purchase Items) === + +func getFlagLabel(flagType utils.FlagType) string { + return "Pembelian " + string(flagType) } -// HppGroup Mappers -func ToHppGroup(groupName string, items []HppItem) HppGroup { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem { + flags := []utils.FlagType{ + utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, + utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, + utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia, + } + + items := []HppItem{} + seenFlags := make(map[utils.FlagType]bool) + + for _, item := range purchaseItems { + if item.Product == nil || len(item.Product.Flags) == 0 { + continue + } + + for _, flag := range item.Product.Flags { + flagType := utils.FlagType(flag.Name) + + // Check if valid flag and not processed + isValid := false + for _, validFlag := range flags { + if validFlag == flagType { + isValid = true + break + } + } + + if isValid && !seenFlags[flagType] { + amount := sumPurchasesByFlag(purchaseItems, flagType) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + + items = append(items, HppItem{ + Type: getFlagLabel(flagType), + Comparison: ToComparison( + ToFinancialMetrics(rpPerBird, rpPerKg, amount), + ToFinancialMetrics(rpPerBird, rpPerKg, amount), // Same for purchase + ), + }) + seenFlags[flagType] = true + } + } + } + + return items +} + +// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === + +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup { + items := []HppItem{} + + // Overhead: all budgets vs (all expenses EXCEPT ekspedisi) + budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightSold) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + + if budgetAmount > 0 || realizationAmount > 0 { + items = append(items, HppItem{ + Type: "Pengeluaran Overhead", + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ), + }) + } + + // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI + ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold) + + if ekspedisiAmount > 0 { + items = append(items, HppItem{ + Type: "Beban Ekspedisi", + Comparison: ToComparison( + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ), + }) + } + return HppGroup{ - GroupName: groupName, + GroupName: "HPP dan Bahan Baku", Data: items, } } -// SummaryHpp Mappers -func ToSummaryHpp(label string, comparison Comparison) SummaryHpp { +// === HPP SUMMARY === + +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp { + // Budget: purchases + budgets + purchaseTotal := sumPurchaseTotal(purchaseItems) + budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + totalBudget := purchaseTotal + budgetTotal + + // Realization: all expenses + totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) + + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold) + return SummaryHpp{ - Label: label, - Comparison: comparison, + Label: label, + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), + ), } } -// HppPurchasesSection Mappers -func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection { + hppGroups := []HppGroup{ + { + GroupName: "HPP dan Pengeluaran", + Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation), + }, + ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation), + } + + summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + return HppPurchasesSection{ - Title: title, Hpp: hppGroups, SummaryHpp: summaryHpp, } } -// PLItem Mappers +// === PROFIT & LOSS === + func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { return PLItem{ Type: itemType, @@ -131,7 +232,6 @@ func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { } } -// PLSummaryItem Mappers func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { return PLSummaryItem{ Label: label, @@ -139,33 +239,106 @@ func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { } } -// PLSummaryGroup Mappers -func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { - return PLSummaryGroup{ - GrossProfit: grossProfit, - SubTotal: subTotal, - NetProfit: netProfit, +func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { + for _, item := range items { + totalAmount += item.Amount + totalPerBird += item.RpPerBird + } + return +} + +func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { + // Categorize deliveries by sales type based on Product flags + categorized := categorizeDeliveriesBySalesType(deliveryProducts) + + items := []PLItem{} + + // Process each sales category + for salesType, deliveries := range categorized { + amount := sumDeliveriesByCategory(deliveries) + + // Use totalPopulation and totalWeightSold for per-unit calculations + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + + items = append(items, ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))) + } + + return items +} + +func ToPembelianItems(purchases []entities.PurchaseItem, totalPopulation, totalWeightSold float64) []PLItem { + amount := sumPurchasesByFilter(purchases, func(item *entities.PurchaseItem) bool { + if item.Product == nil || len(item.Product.Flags) == 0 { + return false + } + for _, flag := range item.Product.Flags { + flagType := strings.ToUpper(flag.Name) + if flagType == string(utils.FlagDOC) || flagType == string(utils.FlagOVK) || flagType == string(utils.FlagPakan) { + return true + } + } + return false + }) + + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } } -// ProfitLossData Mappers -func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { +func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { + realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), + } +} + +func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { + amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + } +} + +func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { + totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) + totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) + totalOverhead, _ := sumPLItems(overheadItems) + totalEkspedisi, _ := sumPLItems(ekspedisiItems) + + grossProfit := totalPenjualan - totalPembelian + grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird + + totalOtherExpenses := totalOverhead + totalEkspedisi + + netProfit := grossProfit - totalOtherExpenses + netProfitPerBird := grossProfitPerBird - 0.0 + + return PLSummaryGroup{ + GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(0, 0, totalOtherExpenses)), + NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), + } +} + +func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { + summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + return ProfitLossData{ - Penjualan: penjualan, - Pembelian: pembelian, + Penjualan: penjualanItems, + Pembelian: pembelianItems, Summary: summary, } } -// ProfitLossSection Mappers -func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection { +func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection { return ProfitLossSection{ - Title: title, - Data: data, + Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems), } } -// ReportResponse Mappers func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { return ReportResponse{ HppPurchases: hppPurchases, @@ -173,14 +346,175 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -// Helper function to create a complete financial report -func BuildFinancialReport( - hppGroups []HppGroup, - summaryHpp SummaryHpp, - penjualan, pembelian []PLItem, - plSummary PLSummaryGroup, -) ReportResponse { - hppSection := ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) - plSection := ToProfitLossSection("Laporan Laba Rugi", ToProfitLossData(penjualan, pembelian, plSummary)) +// === MAIN BUILDER === + +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse { + var totalPopulation float64 + var totalWeightSold float64 + + for _, chickin := range chickins { + totalPopulation += chickin.UsageQty + } + + for _, delivery := range deliveryProducts { + totalWeightSold += delivery.TotalWeight + } + + hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + + penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) + pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold) + overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold) + ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold) + plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + return ToReportResponse(hppSection, plSection) } + +// === HELPER FUNCTIONS === + +func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) { + if totalPopulation > 0 { + rpPerBird = amount / totalPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + return rpPerBird, rpPerKg +} + +func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { + return func(item *entities.PurchaseItem) bool { + if item.Product == nil || len(item.Product.Flags) == 0 { + return false + } + for _, flag := range item.Product.Flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false + } +} + +func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { + return func(realization *entities.ExpenseRealization) bool { + if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil { + return false + } + nonstock := realization.ExpenseNonstock.Nonstock + for _, flag := range nonstock.Flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false + } +} + +func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { + hasFlag := filterRealizationByNonstockFlag(flagType) + return func(realization *entities.ExpenseRealization) bool { + return !hasFlag(realization) + } +} + +func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { + amount := 0.0 + for i := range purchases { + if filter(&purchases[i]) { + amount += purchases[i].TotalPrice + } + } + return amount +} + +func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 { + return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType)) +} + +func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 { + amount := 0.0 + for i := range purchases { + amount += purchases[i].TotalPrice + } + return amount +} + +func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 { + amount := 0.0 + for i := range budgets { + if filter(&budgets[i]) { + amount += budgets[i].Price * budgets[i].Qty + } + } + return amount +} + +func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 { + amount := 0.0 + for i := range realizations { + if filter(&realizations[i]) { + amount += realizations[i].Price * realizations[i].Qty + } + } + return amount +} + +func isChickenProductFlag(flagType utils.FlagType) bool { + switch flagType { + case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, + utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati: + return true + } + return false +} + +func isEggProductFlag(flagType utils.FlagType) bool { + switch flagType { + case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah, + utils.FlagTelurPutih, utils.FlagTelurRetak: + return true + } + return false +} + +func getSalesTypeFromProductFlags(product *entities.Product) string { + if product == nil || len(product.Flags) == 0 { + return "Penjualan Ayam Besar" + } + + for _, flag := range product.Flags { + flagType := utils.FlagType(strings.ToUpper(flag.Name)) + + if isEggProductFlag(flagType) { + return "Penjualan Telur" + } + if isChickenProductFlag(flagType) { + return "Penjualan Ayam Besar" + } + } + + return "Penjualan Ayam Besar" +} + +func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { + categorized := make(map[string][]entities.MarketingDeliveryProduct) + + for _, delivery := range deliveries { + product := delivery.MarketingProduct.ProductWarehouse.Product + salesType := getSalesTypeFromProductFlags(&product) + + categorized[salesType] = append(categorized[salesType], delivery) + } + + return categorized +} + +func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 { + amount := 0.0 + for _, delivery := range deliveries { + amount += delivery.TotalPrice + } + return amount +} diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index c3de4a86..494f2736 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -13,6 +13,7 @@ import ( rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -30,10 +31,11 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) + purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index e6e74d45..29001149 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -15,6 +15,7 @@ import ( marketingRepository "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" + purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -45,9 +46,10 @@ type closingService struct { ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository + PurchaseRepo purchaseRepository.PurchaseRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -59,6 +61,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, + PurchaseRepo: purchaseRepo, } } @@ -386,24 +389,35 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } - _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { + _, err := s.ProjectFlockRepo.GetByID(ctx, id, nil) + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return err == nil, err + }}, + ); err != nil { + return nil, err } + + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) if err != nil { - s.Log.Errorf("Failed to get project flock %d for closing keuangan: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) - if err != nil { - s.Log.Errorf("Failed to get budgets for project flock %d: %+v", projectFlockID, err) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } + purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(c.Context(), projectFlockID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch purchase items") + } + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { - s.Log.Errorf("Failed to get realizations for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } @@ -413,204 +427,15 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* Preload("MarketingProduct.ProductWarehouse.Product") }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get delivery products for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { - s.Log.Errorf("Failed to get chickins for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } - var totalPopulation float64 - for _, chickin := range chickins { - totalPopulation += chickin.UsageQty - } - - var totalWeightSold float64 - for _, delivery := range deliveryProducts { - totalWeightSold += delivery.TotalWeight - } - - hppItems := s.buildHppItems(budgets, realizations, totalWeightSold, totalPopulation) - hppGroups := []dto.HppGroup{ - dto.ToHppGroup("Input Produksi", hppItems), - } - - summaryHpp := s.calculateHppSummary(budgets, realizations, totalWeightSold, totalPopulation) - - penjualanItems := s.buildPenjualanItems(deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := s.buildPembelianItems(budgets, realizations, totalPopulation, totalWeightSold) - plSummary := s.calculatePLSummary(penjualanItems, pembelianItems) - - hppSection := dto.ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) - plSection := dto.ToProfitLossSection("Laporan Laba Rugi", dto.ToProfitLossData(penjualanItems, pembelianItems, plSummary)) - - report := dto.ToReportResponse(hppSection, plSection) + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins) return &report, nil } - -func (s closingService) buildHppItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) []dto.HppItem { - var totalBudgetAmount float64 - var totalRealizationAmount float64 - - for _, budget := range budgets { - totalBudgetAmount += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealizationAmount += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudgetAmount / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudgetAmount / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealizationAmount / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealizationAmount / totalWeightSold - } - - items := []dto.HppItem{ - dto.ToHppItem("Total HPP Produksi", dto.ToComparison( - dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudgetAmount), - dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealizationAmount), - )), - } - - return items -} - -func (s closingService) calculateHppSummary(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) dto.SummaryHpp { - var totalBudget float64 - var totalRealization float64 - - for _, budget := range budgets { - totalBudget += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealization += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudget / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudget / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealization / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealization / totalWeightSold - } - - return dto.ToSummaryHpp("Total HPP", dto.ToComparison( - dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), - dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), - )) -} - -func (s closingService) buildPenjualanItems(deliveryProducts []entity.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []dto.PLItem { - var totalAmount float64 - - for _, delivery := range deliveryProducts { - totalAmount += delivery.TotalPrice - } - - rpPerBird := 0.0 - rpPerKg := 0.0 - if totalPopulation > 0 { - rpPerBird = totalAmount / totalPopulation - } - if totalWeightSold > 0 { - rpPerKg = totalAmount / totalWeightSold - } - - items := []dto.PLItem{ - dto.ToPLItem("Penjualan", dto.ToFinancialMetrics(rpPerBird, rpPerKg, totalAmount)), - } - - return items -} - -func (s closingService) buildPembelianItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalPopulation, totalWeightSold float64) []dto.PLItem { - var totalBudget float64 - var totalRealization float64 - - for _, budget := range budgets { - totalBudget += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealization += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudget / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudget / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealization / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealization / totalWeightSold - } - - items := []dto.PLItem{ - dto.ToPLItem("Beban Pokok Produksi", dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget)), - dto.ToPLItem("Realisasi Beban Pokok", dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization)), - } - - return items -} - -func (s closingService) calculatePLSummary(penjualanItems, pembelianItems []dto.PLItem) dto.PLSummaryGroup { - var totalPenjualan float64 - var totalPenjualanPerBird float64 - var totalPembelian float64 - var totalPembelianPerBird float64 - - for _, item := range penjualanItems { - totalPenjualan += item.Amount - totalPenjualanPerBird += item.RpPerBird - } - - for _, item := range pembelianItems { - totalPembelian += item.Amount - totalPembelianPerBird += item.RpPerBird - } - - grossProfit := totalPenjualan - totalPembelian - grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird - - return dto.ToPLSummaryGroup( - dto.ToPLSummaryItem("Laba Kotor", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - dto.ToPLSummaryItem("Sub Total", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - dto.ToPLSummaryItem("Laba Bersih", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - ) -} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index d1931cdd..474b2962 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -44,6 +44,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock"). Preload("ExpenseNonstock.Nonstock"). Preload("ExpenseNonstock.Nonstock.Uom"). + Preload("ExpenseNonstock.Nonstock.Flags"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). @@ -66,7 +67,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context Preload("Expense.Supplier"). Preload("Kandang"). Preload("Kandang.Location"). - Preload("Nonstock") + Preload("Nonstock"). + Preload("Nonstock.Flags") }). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 8d895e34..b908681e 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -91,7 +91,8 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). - Preload("ProductWarehouse.ProjectFlockKandang") + Preload("ProductWarehouse.ProjectFlockKandang"). + Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 60457074..4a7e627c 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -45,6 +45,9 @@ type RecordingRepository interface { GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) + GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) + GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) + GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) } type RecordingRepositoryImpl struct { @@ -363,6 +366,74 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint return weight, true, nil } +func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { + if projectFlockID == 0 { + return 0, 0, nil + } + + // Get total chickin quantity for this ProjectFlock + totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Get total depletion for this ProjectFlock + totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Calculate actual quantity produced + actualQty := totalChickinQty - totalDepletion + + // Get latest average weight from RecordingBW + avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Calculate total weight + totalWeight = actualQty * avgWeight + + return totalWeight, actualQty, nil +} + +func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID 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.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_bws"). + Select("COALESCE(AVG(recording_bws.avg_weight), 0)"). + Joins("JOIN recordings ON recordings.id = recording_bws.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings WHERE project_flock_kandangs_id = project_flock_kandangs.id)"). + Scan(&result).Error + return result, err +} + func nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 9f008b0d..2f9b2774 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -25,6 +25,7 @@ type PurchaseRepository interface { NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error + GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } type PurchaseRepositoryImpl struct { @@ -289,6 +290,17 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, return count > 0, nil } +func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + var items []entity.PurchaseItem + err := r.DB().WithContext(ctx). + Preload("Product"). + Preload("Product.Flags"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = purchase_items.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Find(&items).Error + return items, err +} + func parseNumericSuffix(value, prefix string) (int, bool) { if !strings.HasPrefix(value, prefix) { return 0, false diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index d00a3ff5..b5285c8e 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -96,28 +96,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { return err } - // Calculate total summary from result items - var total *dto.Summary - if len(result) > 0 { - totalQty := 0 - totalWeightKg := 0.0 - totalSalesAmount := int64(0) - totalHppAmount := int64(0) - - for _, item := range result { - totalQty += int(item.Qty) - totalWeightKg += item.TotalWeightKg - totalSalesAmount += int64(item.SalesAmount) - totalHppAmount += int64(item.HppAmount) - } - - total = &dto.Summary{ - TotalQty: totalQty, - TotalWeightKg: totalWeightKg, - TotalSalesAmount: totalSalesAmount, - TotalHppAmount: totalHppAmount, - } - } + + total := dto.ToSummaryFromDTOItems(result) return ctx.Status(fiber.StatusOK). JSON(MarketingReportResponse{ diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index dc4baabd..deadf3b8 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -50,7 +50,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK agingDays := 0 if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { soDate = mdp.MarketingProduct.Marketing.SoDate - agingDays = int(time.Now().Sub(soDate).Hours() / 24) + agingDays = int(time.Since(soDate).Hours() / 24) } realizationDate := time.Time{} @@ -113,6 +113,20 @@ func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPrice return items } +func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + hppPerKg := float64(0) + if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + } + items = append(items, ToRepportMarketingItemDTO(mdp, hppPerKg)) + } + return items +} + func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { if len(mdps) == 0 { return nil @@ -153,6 +167,37 @@ func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) } +func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { + if len(items) == 0 { + return nil + } + + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, item := range items { + totalQty += int(item.Qty) + totalWeightKg += item.TotalWeightKg + totalSalesAmount += int64(item.SalesAmount) + totalHppAmount += int64(item.HppAmount) + } + + totalHppPricePerKg := float64(0) + if totalWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + } + + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, + } +} + func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) total := ToSummary(mdps, hppPricePerKg) diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..f347ab69 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -11,6 +11,8 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" ) type RepportModule struct{} @@ -19,10 +21,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) + purchaseRepository := purchaseRepo.NewPurchaseRepository(db) + recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 553fc7af..5458a28d 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -12,6 +12,8 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -29,15 +31,19 @@ type repportService struct { Validate *validator.Validate ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository + PurchaseRepo purchaseRepo.PurchaseRepository + RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, + PurchaseRepo: purchaseRepo, + RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, } } @@ -94,7 +100,7 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) - items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) + items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) return items, total, nil } @@ -118,24 +124,33 @@ func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.Market func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { hppMap := make(map[uint]float64) for _, projectFlockID := range projectFlockIDs { - hppPerKg := s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + category := s.getProjectFlockCategory(deliveryProducts, projectFlockID) + hppPerKg := s.calculateHppByCategory(ctx, category, projectFlockID, deliveryProducts) hppMap[projectFlockID] = hppPerKg } return hppMap } -func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO { - items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts)) +func (s *repportService) calculateHppByCategory(ctx context.Context, category string, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { + switch utils.ProjectFlockCategory(category) { + case utils.ProjectFlockCategoryGrowing: + return s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + case utils.ProjectFlockCategoryLaying: + return 0 + default: + return 0 + } +} + +func (s *repportService) getProjectFlockCategory(deliveryProducts []entity.MarketingDeliveryProduct, projectFlockID uint) string { for _, dp := range deliveryProducts { - hppPerKg := float64(0) if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { - hppPerKg = hpp + if projectFlockKandang.ProjectFlockId == projectFlockID { + return projectFlockKandang.ProjectFlock.Category } } - items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg)) } - return items + return "" } func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { @@ -143,17 +158,22 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc return 0 } - realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { - return 0 + s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) } - if len(realizations) == 0 { - return 0 + costPurchase := float64(0) + for _, item := range purchaseItems { + costPurchase += item.TotalPrice + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("GetByProjectFlockID error: %v", err) } costBop := float64(0) - for _, realization := range realizations { cost := realization.Price * realization.Qty category := "" @@ -166,24 +186,21 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc } } - totalActualCost := costBop + totalActualCost := costPurchase + costBop if totalActualCost == 0 { return 0 } - totalWeightSold := float64(0) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && - dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID { - totalWeightSold += dp.TotalWeight - } + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - if totalWeightSold == 0 { + if totalWeightProduced == 0 { return 0 } - hppPerKg := totalActualCost / totalWeightSold + hppPerKg := totalActualCost / totalWeightProduced return hppPerKg }