diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 9dfae460..ed3cfcbc 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -354,6 +354,34 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { }) } +func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error { + projectParam := c.Params("project_flock_id") + kandangParam := c.Params("project_flock_kandang_id") + + projectFlockID, err := strconv.Atoi(projectParam) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pfkID, err := strconv.Atoi(kandangParam) + if err != nil || pfkID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + + result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get closing keuangan by kandang successfully", + Data: result, + }) +} + func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { param := c.Params("project_flock_id") diff --git a/internal/modules/closings/dto/closingKeuanganNew.dto.go b/internal/modules/closings/dto/closingKeuanganNew.dto.go index 4bef7280..6ca19d5c 100644 --- a/internal/modules/closings/dto/closingKeuanganNew.dto.go +++ b/internal/modules/closings/dto/closingKeuanganNew.dto.go @@ -1,5 +1,29 @@ package dto +// === CLOSING KEUANGAN CODES === + +// Closing HPP Codes +type ClosingHPPCode string + +const ( + HPPCodePakan ClosingHPPCode = "PAKAN" + HPPCodeOVK ClosingHPPCode = "OVK" + HPPCodeDOC ClosingHPPCode = "DOC" + HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI" + HPPCodeOverhead ClosingHPPCode = "OVERHEAD" + HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI" +) + +// Closing Profit Loss Codes +type ClosingProfitLossCode string + +const ( + 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 diff --git a/internal/modules/closings/repositories/closingKeuangan.repository.go b/internal/modules/closings/repositories/closingKeuangan.repository.go index 3763f92b..dedea807 100644 --- a/internal/modules/closings/repositories/closingKeuangan.repository.go +++ b/internal/modules/closings/repositories/closingKeuangan.repository.go @@ -17,6 +17,12 @@ type ClosingKeuanganRepository 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 } @@ -310,6 +316,49 @@ func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangI 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/route.go b/internal/modules/closings/route.go index 89578aeb..f0a6ca2a 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -34,5 +34,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang) route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi) route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan) + route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang) } diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index d041d765..ffb7dbf4 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -21,7 +21,8 @@ import ( // ClosingKeuanganService handles closing keuangan business logic type ClosingKeuanganService interface { - GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganResponse, error) + GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) + GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) } type closingKeuanganService struct { @@ -59,8 +60,7 @@ func NewClosingKeuanganService( } } -func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganResponse, error) { - s.Log.Infof("===== START GetClosingKeuangan for ProjectFlockID: %d =====", projectFlockID) +func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, @@ -72,7 +72,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - s.Log.Infof("ProjectFlock: ID=%d, Name=%s, Category=%s", projectFlock.Id, projectFlock.FlockName, projectFlock.Category) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -89,18 +88,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID Preload("Nonstock.Flags"). Where("id IN ?", budgetIDs). Find(&budgets).Error - if err != nil { - s.Log.Warnf("Failed to preload Nonstock.Flags: %v", err) - } - } - - s.Log.Infof("Budgets fetched: %d items", len(budgets)) - for i, b := range budgets { - nonstockName := "Unknown" - if b.Nonstock != nil { - nonstockName = b.Nonstock.Name - } - s.Log.Infof(" Budget[%d]: ID=%d, Nonstock=%s, Price=%.2f, Qty=%.2f", i, b.Id, nonstockName, b.Price, b.Qty) } // Get all kandang for this project flock @@ -108,37 +95,68 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } - s.Log.Infof("Kandangs fetched: %d kandangs", len(kandangs)) - for i, k := range kandangs { - s.Log.Infof(" Kandang[%d]: ID=%d, KandangID=%d, Period=%d", i, k.Id, k.KandangId, k.Period) + + return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) +} + +func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) { + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { + return nil, err } - // Define flag filters - // PAKAN flags: PAKAN, PRE-STARTER, STARTER, FINISHER (priority 1) - // OVK flags: OVK, OBAT, VITAMIN, KIMIA, EKSPEDISI (priority 2) - // AYAM flags: DOC, PULLET, LAYER (priority 3 - only if no PAKAN and OVK) - pakanFilters := []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"} - ovkFilters := []string{"OVK", "OBAT", "VITAMIN", "KIMIA", "EKSPEDISI"} - ayamFilters := []string{"DOC", "PULLET", "LAYER"} + // Validate and fetch project flock kandang + kandang, 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 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock") + } + + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) + if err != nil { + 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 + } + + kandangs := []entity.ProjectFlockKandang{*kandang} + + return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) +} + +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), string(utils.FlagEkspedisi)} + ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)} allFilters := append(pakanFilters, ovkFilters...) allFilters = append(allFilters, ayamFilters...) - s.Log.Infof("All Filters: %v", allFilters) var allProductUsageRows []repository.ProductUsageRow - // Get ALL product usage once - s.Log.Infof("===== Fetching ALL product usage =====") + // Get ALL product usage for _, kandang := range kandangs { - s.Log.Infof("Fetching ALL for kandang ID=%d", kandang.Id) rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters) - if err != nil { - s.Log.Errorf("Failed to get product usage for kandang %d: %v", kandang.Id, err) - } else { - s.Log.Infof("Kandang %d: Got %d rows", kandang.Id, len(rows)) - for i, row := range rows { - s.Log.Infof(" [%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f", - i, row.ProductID, row.ProductName, row.FlagNames, row.TotalQty, row.Price, row.TotalPengeluaran) - } + if err == nil { allProductUsageRows = append(allProductUsageRows, rows...) } } @@ -148,7 +166,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID var ovkProductUsageRows []repository.ProductUsageRow var ayamProductUsageRows []repository.ProductUsageRow - s.Log.Infof("===== Classifying products by flag priority =====") for _, row := range allProductUsageRows { // Parse flag names from comma-separated string flagNames := strings.Split(row.FlagNames, ",") @@ -171,29 +188,17 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID } // Priority: PAKAN > OVK > AYAM - category := "" if hasPakanFlag { pakanProductUsageRows = append(pakanProductUsageRows, row) - category = "PAKAN" } else if hasOvkFlag { ovkProductUsageRows = append(ovkProductUsageRows, row) - category = "OVK" } else if hasAyamFlag { ayamProductUsageRows = append(ayamProductUsageRows, row) - category = "AYAM" } else { - s.Log.Warnf("ProductID=%d (%s) has no recognized flags: %s", row.ProductID, row.ProductName, row.FlagNames) continue } - - s.Log.Infof("ProductID=%d (%s) → Category: %s (Flags: %s, Pakan=%v, OVK=%v, Ayam=%v)", - row.ProductID, row.ProductName, category, row.FlagNames, hasPakanFlag, hasOvkFlag, hasAyamFlag) } - s.Log.Infof("Total ProductUsageRows collected: %d items", len(allProductUsageRows)) - s.Log.Infof(" - PAKAN: %d items", len(pakanProductUsageRows)) - s.Log.Infof(" - OVK: %d items", len(ovkProductUsageRows)) - s.Log.Infof(" - AYAM: %d items", len(ayamProductUsageRows)) // Calculate total price for each category var totalPakanPrice, totalOvkPrice, totalAyamPrice float64 @@ -206,85 +211,100 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID for _, row := range ayamProductUsageRows { totalAyamPrice += row.TotalPengeluaran } - s.Log.Infof("===== TOTAL PRICE BY CATEGORY =====") - s.Log.Infof(" - PAKAN Total Price: %.2f", totalPakanPrice) - s.Log.Infof(" - OVK Total Price: %.2f", totalOvkPrice) - s.Log.Infof(" - AYAM Total Price: %.2f", totalAyamPrice) - s.Log.Infof(" - ALL Total Price: %.2f", totalPakanPrice+totalOvkPrice+totalAyamPrice) - realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) + // Determine if this is per-kandang or per-project-flock scope + isPerKandang := len(kandangs) == 1 + var projectFlockKandangID *uint + if isPerKandang { + kandangID := kandangs[0].Id + projectFlockKandangID = &kandangID + } + + 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) + } if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } - s.Log.Infof("Realizations fetched: %d items", len(realizations)) - for i, r := range realizations { - s.Log.Infof(" Realization[%d]: ID=%d, Price=%.2f, Qty=%.2f", i, r.Id, r.Price, r.Qty) - } - deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { - return db.Preload("MarketingProduct"). + 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") } - s.Log.Infof("DeliveryProducts fetched: %d items", len(deliveryProducts)) - for i, dp := range deliveryProducts { - s.Log.Infof(" DeliveryProduct[%d]: ID=%d, TotalWeight=%.2f, TotalPrice=%.2f", i, dp.Id, dp.TotalWeight, dp.TotalPrice) + + // 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 } - chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) + // 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) + } if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } - s.Log.Infof("Chickins fetched: %d items", len(chickins)) - for i, ch := range chickins { - s.Log.Infof(" Chickin[%d]: ID=%d, UsageQty=%.2f", i, ch.Id, ch.UsageQty) - } // Get total depletion - totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + 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 { - s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) totalDepletion = 0 } - s.Log.Infof("TotalDepletion: %.2f birds", totalDepletion) - totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id) if err != nil { - s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - s.Log.Infof("TotalWeightProduced (from stub): %.2f kg", totalWeightProduced) // Try to get actual weight from uniformity data - totalWeightFromUniformity, err := s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlockID) + 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 { - s.Log.Warnf("GetTotalWeightProducedFromUniformityByProjectFlockID error: %v", err) } else if totalWeightFromUniformity > 0 { totalWeightProduced = totalWeightFromUniformity - s.Log.Infof("TotalWeightProduced (from uniformity): %.2f kg", totalWeightProduced) } // Fetch egg data only for Laying category var totalEggWeightKg float64 if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - s.Log.Infof("===== Fetching EGG data (Laying Category) =====") // TODO: Replace with actual method to get egg weight from RecordingRepo - // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlockID) + // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id) // For now, set to 0 as placeholder totalEggWeightKg = 0 - s.Log.Infof("TotalEggWeightKg: %.2f kg", totalEggWeightKg) - if err != nil { - s.Log.Warnf("Failed to fetch egg weight: %v", err) - } } else { - s.Log.Infof("Skipping egg data fetch (Category: %s - not Laying)", projectFlock.Category) totalEggWeightKg = 0 } // Build new DTO structure - s.Log.Infof("===== BUILDING NEW DTO RESPONSE =====") // Calculate totals var totalPopulation float64 @@ -294,7 +314,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID // Calculate actual population (total population - depletion) actualPopulation := totalPopulation - totalDepletion - s.Log.Infof("Population - Total: %.2f, Depletion: %.2f, Actual: %.2f", totalPopulation, totalDepletion, actualPopulation) // Calculate budget totals by category calculateBudgetByFlag := func(flags []string) float64 { @@ -328,12 +347,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID } budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi - s.Log.Infof("Budgets by category:") - s.Log.Infof(" PAKAN: %.2f", budgetPakan) - s.Log.Infof(" OVK: %.2f", budgetOvk) - s.Log.Infof(" AYAM: %.2f", budgetAyam) - s.Log.Infof(" OVERHEAD: %.2f", budgetOperational) - s.Log.Infof(" EKSPEDISI: %.2f", budgetEkspedisi) // Calculate realization totals var totalRealizationAmount float64 @@ -398,7 +411,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID } } - s.Log.Infof("Filtered DeliveryProducts: %d items (from %d total)", len(filteredDeliveryProducts), len(deliveryProducts)) // Calculate total weight sold and sales amount from filtered products var totalWeightSold float64 @@ -408,14 +420,6 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID totalSalesAmount += delivery.TotalPrice } - s.Log.Infof("Calculated totals:") - s.Log.Infof(" Total Population: %.2f", totalPopulation) - s.Log.Infof(" Actual Population: %.2f (after depletion)", actualPopulation) - s.Log.Infof(" Total Weight Produced: %.2f kg (ayam)", totalWeightProduced) - s.Log.Infof(" Total Weight Sold: %.2f kg (filtered by category)", totalWeightSold) - s.Log.Infof(" Total Budget: %.2f", totalBudgetAmount) - s.Log.Infof(" Total Realization: %.2f (Operational: %.2f, Ekspedisi: %.2f)", totalRealizationAmount, totalOperationalRealization, totalEkspedisiRealization) - s.Log.Infof(" Total Sales: %.2f", totalSalesAmount) // Calculate metrics - always use kg ayam for rp_per_kg calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { @@ -439,7 +443,7 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID return } - // Build HPP Items + // Build HPP Items using constants hppItems := []dto.HPPItem{} // PAKAN item @@ -448,8 +452,8 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID hppItems = append(hppItems, dto.ToHPPItem( 1, "purchase", - "PAKAN", - "Pakan", + string(dto.HPPCodePakan), + "Pembelian Pakan", dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan), dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice), )) @@ -460,43 +464,49 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID hppItems = append(hppItems, dto.ToHPPItem( 2, "purchase", - "OVK", - "OVK", + string(dto.HPPCodeOVK), + "Pembelian OVK", dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk), dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice), )) - // DOC item + // 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", - "DOC", - "DOC", + docCode, + docLabel, dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam), dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice), )) - // OVERHEAD operational item (before EKSPEDISI) + // OVERHEAD item overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational) overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization) hppItems = append(hppItems, dto.ToHPPItem( 4, "overhead", - "OVERHEAD", + string(dto.HPPCodeOverhead), "Pengeluaran Overhead", dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational), dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization), )) - // EKSPEDISI item (overhead) + // EKSPEDISI item ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi) ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization) hppItems = append(hppItems, dto.ToHPPItem( 5, "overhead", - "EKSPEDISI", + string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi), dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization), @@ -535,7 +545,7 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID hppSection := dto.ToHPPSection(hppItems, hppSummary) - // Build Profit Loss Items + // Build Profit Loss Items using constants plItems := []dto.ProfitLossItem{} // SALES item @@ -545,7 +555,7 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID salesLabel = "Penjualan Telur" } plItems = append(plItems, dto.ToProfitLossItem( - "SALES", + string(dto.PLCodeSales), salesLabel, "income", salesRpPerBird, @@ -553,44 +563,24 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID totalSalesAmount, )) - // PURCHASE_DOC item - purchaseDocLabel := "Pembelian DOC" - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - purchaseDocLabel = "Depresiasi" - } + // SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK + totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice + sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird + sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg + sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( - "PURCHASE_DOC", - purchaseDocLabel, + string(dto.PLCodeSapronak), + sapronakLabel, "purchase", - docRealizationRpPerBird, - docRealizationRpPerKg, - totalAyamPrice, - )) - - // PAKAN item - plItems = append(plItems, dto.ToProfitLossItem( - "PAKAN", - "Pakan", - "purchase", - pakanRealizationRpPerBird, - pakanRealizationRpPerKg, - totalPakanPrice, - )) - - // OVK item - plItems = append(plItems, dto.ToProfitLossItem( - "OVK", - "OVK", - "purchase", - ovkRealizationRpPerBird, - ovkRealizationRpPerKg, - totalOvkPrice, + sapronakRpPerBird, + sapronakRpPerKg, + totalSapronakAmount, )) // OVERHEAD item overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization) plItems = append(plItems, dto.ToProfitLossItem( - "OVERHEAD", + string(dto.PLCodeOverhead), "Overhead", "overhead", overheadRpPerBird, @@ -600,7 +590,7 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID // EKSPEDISI item plItems = append(plItems, dto.ToProfitLossItem( - "EKSPEDISI", + string(dto.PLCodeEkspedisi), "Ekspedisi", "overhead", ekspedisiRealizationRpPerBird, @@ -609,21 +599,21 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID )) // Profit Loss Summary - // Calculate total cost of goods sold (HPP) - use realization - totalCostOfGoodsSold := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization - totalCostOfGoodsSoldRpPerBird := pakanRealizationRpPerBird + ovkRealizationRpPerBird + docRealizationRpPerBird + overheadRpPerBird + ekspedisiRealizationRpPerBird + // Gross Profit = Sales - (DOC + PAKAN + OVK) only + // Gross Profit should NOT include overhead and ekspedisi + costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice + costOfGoodsSoldRpPerBird := sapronakRpPerBird - // Gross Profit = Sales - Cost of Goods Sold - grossProfit := totalSalesAmount - totalCostOfGoodsSold - grossProfitRpPerBird := salesRpPerBird - totalCostOfGoodsSoldRpPerBird + grossProfit := totalSalesAmount - costOfGoodsSold + grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird - // Operating Expenses (already included in COGS above, so this shows the breakdown) + // Operating Expenses (Overhead + Ekspedisi) totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird - // Net Profit = Gross Profit (COGS already deducted) - netProfit := grossProfit - netProfitRpPerBird := grossProfitRpPerBird + // Net Profit = Gross Profit - Operating Expenses + netProfit := grossProfit - totalOperatingExpenses + netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird plSummary := dto.ToProfitLossSummary( dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit), @@ -635,13 +625,8 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID // Build complete response data := dto.ToClosingKeuanganData(hppSection, profitLossSection) - response := dto.ToSuccessClosingKeuanganResponse(data) - s.Log.Infof("===== NEW DTO RESPONSE BUILT SUCCESSFULLY =====") - s.Log.Infof("HPP Items: %d, Profit Loss Items: %d", len(hppItems), len(plItems)) - s.Log.Infof("===== END GetClosingKeuangan for ProjectFlockID: %d =====", projectFlockID) - - return &response, nil + return &data, nil } // containsItem checks if a string exists in a slice diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index db43b2fa..703c05f0 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -563,16 +563,15 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF err := r.DB().WithContext(ctx). Table("project_flock_kandang_uniformity"). - Select("COALESCE(SUM(mean_weight * chick_qty_of_weight / 1000), 0) as total_weight"). + Select("COALESCE(SUM((mean_up / 1.10) * chick_qty_of_weight / 1000), 0) as total_weight"). Joins("JOIN ("+ " SELECT pfku.project_flock_kandang_id, MAX(pfku.id) as latest_id "+ " FROM project_flock_kandang_uniformity pfku "+ " JOIN project_flock_kandangs pfk ON pfk.id = pfku.project_flock_kandang_id "+ " WHERE pfk.project_flock_id = ? "+ - " AND pfku.deleted_at IS NULL "+ " GROUP BY pfku.project_flock_kandang_id "+ ") latest ON latest.project_flock_kandang_id = project_flock_kandang_uniformity.project_flock_kandang_id "+ - "AND project_flock_kandang_uniformity.id = latest.latest_id"). + "AND project_flock_kandang_uniformity.id = latest.latest_id", projectFlockID). Scan(&result).Error return result.TotalWeight, err