From afe4b2ffe395ae05d42c436a7a139a632213aaf8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 16 Dec 2025 21:10:48 +0700 Subject: [PATCH 01/11] feat[BE}: change get penjualan repport dto an add more params --- .../controllers/closing.controller.go | 22 ++ .../closings/dto/closingKeuangan.dto.go | 186 +++++++++++++ internal/modules/closings/route.go | 2 + .../closings/services/closing.service.go | 235 ++++++++++++++++ .../expense_realization.repository.go | 6 +- .../salesorder_delivery_product.repository.go | 119 ++++++-- .../controllers/repport.controller.go | 24 +- .../repports/dto/repportMarketing.dto.go | 260 ++++++------------ .../repports/services/repport.service.go | 112 ++++++-- .../validations/repport.validation.go | 22 +- 10 files changed, 741 insertions(+), 247 deletions(-) create mode 100644 internal/modules/closings/dto/closingKeuangan.dto.go diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index a04fc5f9..bca2f9cb 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -245,3 +245,25 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { Data: payload, }) } + +func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + } + + result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get closing keuangan successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go new file mode 100644 index 00000000..d380dc3d --- /dev/null +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -0,0 +1,186 @@ +package dto + +// === BASE METRICS === +type FinancialMetrics struct { + RpPerBird float64 `json:"rp_per_bird"` + RpPerKg float64 `json:"rp_per_kg"` + Amount float64 `json:"amount"` +} + +type Comparison struct { + Budgeting FinancialMetrics `json:"budgeting"` + Realization FinancialMetrics `json:"realization"` +} + +// === HPP PURCHASES PACKAGE === +type HppItem struct { + Type string `json:"type"` + Comparison +} + +type HppGroup struct { + GroupName string `json:"group_name"` + Data []HppItem `json:"data"` +} + +type SummaryHpp struct { + Label string `json:"label"` + 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"` +} + +// === PROFIT LOSS PACKAGE === +type PLItem struct { + Type string `json:"type"` + FinancialMetrics +} + +type PLSummaryItem struct { + Label string `json:"label"` + FinancialMetrics +} + +type PLSummaryGroup struct { + GrossProfit PLSummaryItem `json:"gross_profit"` + SubTotal PLSummaryItem `json:"sub_total"` + NetProfit PLSummaryItem `json:"net_profit"` +} + +type ProfitLossData struct { + Penjualan []PLItem `json:"penjualan"` + Pembelian []PLItem `json:"pembelian"` + Summary PLSummaryGroup `json:"summary"` +} + +// Ini adalah struct mandiri untuk bagian Profit Loss +type ProfitLossSection struct { + Title string `json:"title"` + 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"` +} + +// === MAPPER FUNCTIONS === + +// FinancialMetrics Mappers +func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { + return FinancialMetrics{ + RpPerBird: rpPerBird, + RpPerKg: rpPerKg, + Amount: amount, + } +} + +// Comparison Mappers +func ToComparison(budgeting, realization FinancialMetrics) Comparison { + return Comparison{ + Budgeting: budgeting, + Realization: realization, + } +} + +// HppItem Mappers +func ToHppItem(itemType string, comparison Comparison) HppItem { + return HppItem{ + Type: itemType, + Comparison: comparison, + } +} + +// HppGroup Mappers +func ToHppGroup(groupName string, items []HppItem) HppGroup { + return HppGroup{ + GroupName: groupName, + Data: items, + } +} + +// SummaryHpp Mappers +func ToSummaryHpp(label string, comparison Comparison) SummaryHpp { + return SummaryHpp{ + Label: label, + Comparison: comparison, + } +} + +// HppPurchasesSection Mappers +func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) HppPurchasesSection { + return HppPurchasesSection{ + Title: title, + Hpp: hppGroups, + SummaryHpp: summaryHpp, + } +} + +// PLItem Mappers +func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { + return PLItem{ + Type: itemType, + FinancialMetrics: metrics, + } +} + +// PLSummaryItem Mappers +func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { + return PLSummaryItem{ + Label: label, + FinancialMetrics: metrics, + } +} + +// PLSummaryGroup Mappers +func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { + return PLSummaryGroup{ + GrossProfit: grossProfit, + SubTotal: subTotal, + NetProfit: netProfit, + } +} + +// ProfitLossData Mappers +func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { + return ProfitLossData{ + Penjualan: penjualan, + Pembelian: pembelian, + Summary: summary, + } +} + +// ProfitLossSection Mappers +func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection { + return ProfitLossSection{ + Title: title, + Data: data, + } +} + +// ReportResponse Mappers +func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { + return ReportResponse{ + HppPurchases: hppPurchases, + ProfitLoss: profitLoss, + } +} + +// 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)) + return ToReportResponse(hppSection, plSection) +} diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index a76e8b79..62998f2c 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,6 +25,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject) + route.Get("/:project_flock_id/keuangan", ctrl.GetClosingKeuangan) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index b1780359..e6e74d45 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -31,6 +31,7 @@ type ClosingService interface { GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) + GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) } type closingService struct { @@ -379,3 +380,237 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove return &result, nil } + +func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { + if projectFlockID == 0 { + 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 != 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) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") + } + + 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") + } + + deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { + return db.Preload("MarketingProduct"). + Preload("MarketingProduct.ProductWarehouse"). + 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) + + 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 e4d57b79..d1931cdd 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -46,10 +46,10 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock.Nonstock.Uom"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Where("expenses.category = ?", "BOP"). + Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). + Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). + Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID). Find(&realizations).Error return realizations, err } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 85d850a6..94d23103 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.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" @@ -31,8 +32,6 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct - // JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas - // Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter 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"). @@ -91,16 +90,17 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Warehouse"). + Preload("ProductWarehouse.ProjectFlockKandang") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } - if filters.ProductId > 0 { + if filters.ProductId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") } @@ -109,8 +109,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.Search != "" { - db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", - "%"+filters.Search+"%") + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + + if filters.Search != "" { + searchPattern := "%" + filters.Search + "%" + db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) } if filters.CustomerId > 0 { @@ -121,10 +126,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) } - if filters.MarketingId > 0 { - db = db.Where("marketings.id = ?", filters.MarketingId) - } - if filters.ProductId > 0 { db = db.Where("product_warehouses.product_id = ?", filters.ProductId) } @@ -133,17 +134,90 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.ProjectFlockKandangId > 0 { - db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) - } - - if filters.DeliveryDate != "" { - if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { - nextDate := deliveryDate.AddDate(0, 0, 1) - db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) + if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { + if filters.FilterBy == "delivery_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.created_at >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.created_at < ?", nextDate) + } + } } } + sortColumn := "marketing_delivery_products.id" + sortOrder := "DESC" + + if filters.SortBy != "" { + switch filters.SortBy { + case "delivery_date": + sortColumn = "marketing_delivery_products.delivery_date" + case "customer": + sortColumn = "customers.name" + if !containsJoin(db, "customers") { + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + case "warehouse": + sortColumn = "warehouses.name" + if !containsJoin(db, "warehouses") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") + } + case "product": + sortColumn = "products.name" + if !containsJoin(db, "products") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") + } + case "sales_person": + sortColumn = "sales_users.name" + if !containsJoin(db, "sales_users") { + db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") + } + case "vehicle_number": + sortColumn = "marketing_delivery_products.vehicle_number" + case "sales_amount": + sortColumn = "marketing_delivery_products.total_price" + case "hpp_amount": + sortColumn = "marketing_delivery_products.total_price" + case "qty": + sortColumn = "marketing_delivery_products.qty" + case "average_weight": + sortColumn = "marketing_delivery_products.avg_weight" + case "total_weight": + sortColumn = "marketing_delivery_products.total_weight" + case "sales_price": + sortColumn = "marketing_delivery_products.unit_price" + case "hpp_price": + sortColumn = "marketing_delivery_products.unit_price" + case "aging_days": + sortColumn = "marketing_delivery_products.delivery_date" + } + } + + if filters.SortOrder != "" && (filters.SortOrder == "asc" || filters.SortOrder == "desc") { + sortOrder = strings.ToUpper(filters.SortOrder) + } + + db = db.Order(sortColumn + " " + sortOrder) + if err := db.Count(&total).Error; err != nil { return nil, 0, err } @@ -151,10 +225,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if err := db. Offset(offset). Limit(limit). - Order("marketing_delivery_products.id DESC"). Find(&deliveryProducts).Error; err != nil { return nil, 0, err } return deliveryProducts, total, nil } + +func containsJoin(db *gorm.DB, tableName string) bool { + statement := db.Statement + joinSQL := statement.SQL.String() + return strings.Contains(joinSQL, "JOIN "+tableName) +} diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..b94ec8c2 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -62,16 +62,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { query := &validation.MarketingQuery{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - Search: ctx.Query("search", ""), - CustomerId: int64(ctx.QueryInt("customer_id", 0)), - ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), - DeliveryDate: ctx.Query("delivery_date", ""), - ProductId: int64(ctx.QueryInt("product_id", 0)), - WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), - SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), - MarketingId: int64(ctx.QueryInt("marketing_id", 0)), + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + Search: ctx.Query("search", ""), + CustomerId: int64(ctx.QueryInt("customer_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), + SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), + FilterBy: ctx.Query("filter_by", ""), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + SortBy: ctx.Query("sort_by", ""), + SortOrder: ctx.Query("sort_order", ""), } if query.Page < 1 || query.Limit < 1 { @@ -84,7 +86,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { } return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ + JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 9cbd57ba..77c5f5d8 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,219 +1,121 @@ package dto import ( - "time" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +// === Main Report Item DTO === -type RepportMarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - Notes string `json:"notes"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type RepportMarketingItemDTO struct { + DoDate string `json:"do_date"` + RealizationDate string `json:"realization_date"` + AgingDays int `json:"aging_days"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + DoNumber string `json:"do_number"` + Sales *userDTO.UserRelationDTO `json:"sales,omitempty"` + VehicleNumber string `json:"vehicle_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + MarketingType string `json:"marketing_type"` + Qty float64 `json:"qty"` + AverageWeightKg float64 `json:"average_weight_kg"` + TotalWeightKg float64 `json:"total_weight_kg"` + SalesPricePerKg float64 `json:"sales_price_per_kg"` + HppPricePerKg float64 `json:"hpp_price_per_kg"` + SalesAmount float64 `json:"sales_amount"` + HppAmount float64 `json:"hpp_amount"` } -type RepportMarketingProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} +// === Report Response DTO === -type RepportMarketingDeliveryDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - VehicleNumber string `json:"vehicle_number"` - DoNumber string `json:"do_number"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -type RepportMarketingListDTO struct { - RepportMarketingBaseDTO - MarketingProduct RepportMarketingProductDTO `json:"marketing_product"` - MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"` - TotalMarketingProduct float64 `json:"total_marketing_product"` - TotalMarketingDelivery float64 `json:"total_marketing_delivery"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` +type RepportMarketingResponseDTO struct { + Items []RepportMarketingItemDTO `json:"items"` } // === MAPPERS === -func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO { - if m == nil { - return RepportMarketingBaseDTO{} +// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { + agingDays := 0 + + doDate := "" + if mdp.DeliveryDate != nil { + doDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var customer *customerDTO.CustomerRelationDTO - if m.Customer.Id != 0 { - mapped := customerDTO.ToCustomerRelationDTO(m.Customer) - customer = &mapped + realizationDate := "" + if mdp.DeliveryDate != nil { + realizationDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var salesPerson *userDTO.UserRelationDTO - if m.SalesPerson.Id != 0 { - mapped := userDTO.ToUserRelationDTO(m.SalesPerson) - salesPerson = &mapped + // Calculate sales_amount = total_weight_kg * sales_price_per_kg + salesAmount := mdp.TotalWeight * mdp.UnitPrice + // Calculate hpp_amount = total_weight_kg * hpp_price_per_kg + hppAmount := mdp.TotalWeight * hppPricePerKg + + item := RepportMarketingItemDTO{ + DoDate: doDate, + RealizationDate: realizationDate, + AgingDays: agingDays, + DoNumber: mdp.MarketingProduct.Marketing.SoNumber, + MarketingType: "ayam", + Qty: mdp.Qty, + AverageWeightKg: mdp.AvgWeight, + TotalWeightKg: mdp.TotalWeight, + SalesPricePerKg: mdp.UnitPrice, + HppPricePerKg: hppPricePerKg, + SalesAmount: salesAmount, + HppAmount: hppAmount, } - return RepportMarketingBaseDTO{ - Id: m.Id, - SoNumber: m.SoNumber, - SoDate: m.SoDate, - Customer: customer, - SalesPerson: salesPerson, - Notes: m.Notes, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, - } -} - -func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO { - if mp == nil { - return RepportMarketingProductDTO{} + // Map warehouse with full details + if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { + mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) + item.Warehouse = &mapped } - var product *productDTO.ProductRelationDTO - if mp.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) - product = &mapped + // Map customer using CustomerRelationDTO + if mdp.MarketingProduct.Marketing.CustomerId != 0 { + mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) + item.Customer = &mapped } - return RepportMarketingProductDTO{ - Id: mp.Id, - MarketingProductId: mp.Id, - Qty: mp.Qty, - UnitPrice: mp.UnitPrice, - AvgWeight: mp.AvgWeight, - TotalWeight: mp.TotalWeight, - TotalPrice: mp.TotalPrice, - Product: product, - CreatedAt: time.Now(), - } -} - -func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO { - if mdp == nil { - return RepportMarketingDeliveryDTO{} + // Map sales person + if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { + mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) + item.Sales = &mapped } - var product *productDTO.ProductRelationDTO - if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { + // Map vehicle number + item.VehicleNumber = mdp.VehicleNumber + + // Map product using ProductRelationDTO + if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) - product = &mapped + item.Product = &mapped } - warehouseId := uint(0) - if mdp.MarketingProduct.ProductWarehouse.Id != 0 { - warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId - } - - doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId) - - return RepportMarketingDeliveryDTO{ - Id: mdp.Id, - MarketingProductId: mdp.MarketingProductId, - Qty: mdp.Qty, - UnitPrice: mdp.UnitPrice, - TotalWeight: mdp.TotalWeight, - AvgWeight: mdp.AvgWeight, - TotalPrice: mdp.TotalPrice, - DeliveryDate: mdp.DeliveryDate, - VehicleNumber: mdp.VehicleNumber, - DoNumber: doNumber, - Product: product, - CreatedAt: time.Now(), - } + return item } -func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO { - var marketingProduct RepportMarketingProductDTO - var marketingDelivery RepportMarketingDeliveryDTO - - if mp != nil { - marketingProduct = ToRepportMarketingProductDTO(mp) - } - - if mdp != nil { - marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber) - } - - totalMarketingProduct := float64(0) - totalMarketingDelivery := float64(0) - - if mp != nil { - totalMarketingProduct = mp.Qty * mp.UnitPrice - } - - if mdp != nil { - totalMarketingDelivery = mdp.Qty * mdp.UnitPrice - } - - return RepportMarketingListDTO{ - RepportMarketingBaseDTO: baseDTO, - MarketingProduct: marketingProduct, - MarketingDelivery: marketingDelivery, - TotalMarketingProduct: totalMarketingProduct, - TotalMarketingDelivery: totalMarketingDelivery, - LatestApproval: latestApproval, +// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg)) } + return items } -func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { - result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) +// ToRepportMarketingResponseDTO creates complete marketing report response +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) - marketingMap := make(map[uint]entity.MarketingDeliveryProduct) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.Marketing.Id == 0 { - continue - } - marketingID := dp.MarketingProduct.Marketing.Id - if _, exists := marketingMap[marketingID]; !exists { - marketingMap[marketingID] = dp - } + return RepportMarketingResponseDTO{ + Items: items, } - - for _, deliveryProduct := range marketingMap { - if deliveryProduct.MarketingProduct.Marketing.Id == 0 { - continue - } - - marketing := &deliveryProduct.MarketingProduct.Marketing - baseDTO := ToRepportMarketingBaseDTO(marketing) - - var latestApproval *approvalDTO.ApprovalRelationDTO - if marketing.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped - } - - mdp := &deliveryProduct - dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval) - result = append(result, dto) - } - - return result } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..4db200ab 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1,6 +1,9 @@ package service import ( + "context" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -18,7 +21,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) - GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) } type repportService struct { @@ -77,7 +80,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer return result, total, nil } -func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { +func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -89,27 +92,88 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - marketingIDMap := make(map[uint]bool) - marketingIDs := make([]uint, 0) - for _, dp := range deliveryProducts { - if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] { - marketingIDs = append(marketingIDs, marketingID) - marketingIDMap[marketingID] = true - } - } + projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) + hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) + items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) - approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("LatestByTargets error: %v", err) - } - - for i := range deliveryProducts { - if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil { - deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval - } - } - - return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil + return items, total, nil +} + +func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint { + projectFlockIDMap := make(map[uint]bool) + projectFlockIDs := make([]uint, 0) + + for _, dp := range deliveryProducts { + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] { + projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId) + projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true + } + } + } + + return projectFlockIDs +} + +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) + 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)) + for _, dp := range deliveryProducts { + hppPerKg := float64(0) + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + } + items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg)) + } + return items +} + +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { + if projectFlockID == 0 { + return 0 + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0 + } + + if len(realizations) == 0 { + return 0 + } + + totalActualCost := float64(0) + for _, realization := range realizations { + cost := realization.Price * realization.Qty + totalActualCost += cost + } + + 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 + } + } + + if totalWeightSold == 0 { + return 0 + } + + hppPerKg := totalActualCost / totalWeightSold + return hppPerKg } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..e568952d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -16,14 +16,16 @@ type ExpenseQuery struct { } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - DeliveryDate string `query:"delivery_date" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - MarketingId int64 `query:"marketing_id" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` } From 40f192660d4d1b361153458baf973ba0084a189d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 17 Dec 2025 11:30:49 +0700 Subject: [PATCH 02/11] Feat[BE]:: adjust marketing report API --- .../salesorder_delivery_product.repository.go | 30 +++--- .../controllers/repport.controller.go | 39 +++++++- .../repports/dto/repportMarketing.dto.go | 92 +++++++++++++------ .../repports/services/repport.service.go | 14 ++- .../validations/repport.validation.go | 4 +- 5 files changed, 131 insertions(+), 48 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 94d23103..8d895e34 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -135,7 +135,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { - if filters.FilterBy == "delivery_date" { + if filters.FilterBy == "so_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.so_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.so_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { if filters.StartDate != "" { if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) @@ -147,18 +159,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) } } - } else if filters.FilterBy == "realization_date" { - if filters.StartDate != "" { - if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { - db = db.Where("marketings.created_at >= ?", startDate) - } - } - if filters.EndDate != "" { - if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { - nextDate := endDate.AddDate(0, 0, 1) - db = db.Where("marketings.created_at < ?", nextDate) - } - } } } @@ -167,7 +167,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if filters.SortBy != "" { switch filters.SortBy { - case "delivery_date": + case "so_date": + sortColumn = "marketings.so_date" + case "realization_date": sortColumn = "marketing_delivery_products.delivery_date" case "customer": sortColumn = "customers.name" diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index b94ec8c2..d00a3ff5 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -11,6 +11,17 @@ import ( "github.com/gofiber/fiber/v2" ) +// === Marketing Report Response === + +type MarketingReportResponse struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta response.Meta `json:"meta"` + Data []dto.RepportMarketingItemDTO `json:"data"` + Total *dto.Summary `json:"total,omitempty"` +} + type RepportController struct { RepportService service.RepportService } @@ -85,8 +96,31 @@ 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, + } + } + return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{ + JSON(MarketingReportResponse{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", @@ -96,6 +130,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: result, + Data: result, + Total: total, }) } diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 77c5f5d8..98ec9888 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,6 +1,9 @@ package dto import ( + "fmt" + "time" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" @@ -8,11 +11,10 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === Main Report Item DTO === - type RepportMarketingItemDTO struct { - DoDate string `json:"do_date"` - RealizationDate string `json:"realization_date"` + ID int `json:"id"` + SoDate time.Time `json:"so_date"` + RealizationDate time.Time `json:"realization_date"` AgingDays int `json:"aging_days"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` @@ -30,70 +32,70 @@ type RepportMarketingItemDTO struct { HppAmount float64 `json:"hpp_amount"` } -// === Report Response DTO === +type Summary struct { + TotalQty int `json:"total_qty"` + TotalWeightKg float64 `json:"total_weight_kg"` + TotalSalesAmount int64 `json:"total_sales_amount"` + TotalHppAmount int64 `json:"total_hpp_amount"` +} type RepportMarketingResponseDTO struct { Items []RepportMarketingItemDTO `json:"items"` + Total *Summary `json:"total,omitempty"` } -// === MAPPERS === - -// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { + soDate := time.Time{} agingDays := 0 - - doDate := "" - if mdp.DeliveryDate != nil { - doDate = mdp.DeliveryDate.Format("02-Jan-2006") + if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { + soDate = mdp.MarketingProduct.Marketing.SoDate + agingDays = int(time.Now().Sub(soDate).Hours() / 24) } - realizationDate := "" + realizationDate := time.Time{} if mdp.DeliveryDate != nil { - realizationDate = mdp.DeliveryDate.Format("02-Jan-2006") + realizationDate = *mdp.DeliveryDate } - // Calculate sales_amount = total_weight_kg * sales_price_per_kg - salesAmount := mdp.TotalWeight * mdp.UnitPrice - // Calculate hpp_amount = total_weight_kg * hpp_price_per_kg - hppAmount := mdp.TotalWeight * hppPricePerKg + doNumber := generateDoNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) + + totalWeightKg := mdp.Qty * mdp.AvgWeight + salesAmount := totalWeightKg * mdp.UnitPrice + hppAmount := totalWeightKg * hppPricePerKg item := RepportMarketingItemDTO{ - DoDate: doDate, + ID: int(mdp.Id), + SoDate: soDate, RealizationDate: realizationDate, AgingDays: agingDays, - DoNumber: mdp.MarketingProduct.Marketing.SoNumber, + DoNumber: doNumber, MarketingType: "ayam", Qty: mdp.Qty, AverageWeightKg: mdp.AvgWeight, - TotalWeightKg: mdp.TotalWeight, + TotalWeightKg: totalWeightKg, SalesPricePerKg: mdp.UnitPrice, HppPricePerKg: hppPricePerKg, SalesAmount: salesAmount, HppAmount: hppAmount, } - // Map warehouse with full details if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) item.Warehouse = &mapped } - // Map customer using CustomerRelationDTO if mdp.MarketingProduct.Marketing.CustomerId != 0 { mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) item.Customer = &mapped } - // Map sales person if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) item.Sales = &mapped } - // Map vehicle number item.VehicleNumber = mdp.VehicleNumber - // Map product using ProductRelationDTO if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) item.Product = &mapped @@ -102,7 +104,6 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK return item } -// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { @@ -111,11 +112,46 @@ func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPrice return items } -// ToRepportMarketingResponseDTO creates complete marketing report response +func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { + if len(mdps) == 0 { + return nil + } + + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, mdp := range mdps { + calculatedTotalWeight := mdp.Qty * mdp.AvgWeight + totalQty += int(mdp.Qty) + totalWeightKg += calculatedTotalWeight + totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) + totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + } + + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + } +} + +func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string { + dateStr := "" + if deliveryDate != nil { + dateStr = deliveryDate.Format("20060102") + } + return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) +} + func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) + total := ToSummary(mdps, hppPricePerKg) return RepportMarketingResponseDTO{ Items: items, + Total: total, } } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 4db200ab..553fc7af 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -152,12 +152,22 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc return 0 } - totalActualCost := float64(0) + costBop := float64(0) + for _, realization := range realizations { cost := realization.Price * realization.Qty - totalActualCost += cost + category := "" + if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil { + category = realization.ExpenseNonstock.Expense.Category + } + + if category == "BOP" { + costBop += cost + } } + totalActualCost := costBop + if totalActualCost == 0 { return 0 } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index e568952d..6b3bd71e 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -23,9 +23,9 @@ type MarketingQuery struct { ProductId int64 `query:"product_id" validate:"omitempty"` WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` } From d9a1372077039d14f03d7ab5f0a00a0889568710 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 17 Dec 2025 11:34:08 +0700 Subject: [PATCH 03/11] feat[BE]:: add totalHppPricePerKg to marketing report summary --- .../repports/dto/repportMarketing.dto.go | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 98ec9888..dc4baabd 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -33,10 +33,11 @@ type RepportMarketingItemDTO struct { } type Summary struct { - TotalQty int `json:"total_qty"` - TotalWeightKg float64 `json:"total_weight_kg"` - TotalSalesAmount int64 `json:"total_sales_amount"` - TotalHppAmount int64 `json:"total_hpp_amount"` + TotalQty int `json:"total_qty"` + TotalWeightKg float64 `json:"total_weight_kg"` + TotalSalesAmount int64 `json:"total_sales_amount"` + TotalHppAmount int64 `json:"total_hpp_amount"` + TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"` } type RepportMarketingResponseDTO struct { @@ -130,11 +131,17 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) } + totalHppPricePerKg := float64(0) + if totalWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + } + return &Summary{ - TotalQty: totalQty, - TotalWeightKg: totalWeightKg, - TotalSalesAmount: totalSalesAmount, - TotalHppAmount: totalHppAmount, + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, } } From 1b238616568f2cba52eba12eb0a49758c5b0522d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 09:58:31 +0700 Subject: [PATCH 04/11] feat[BE]: membetulkan perhitungan hpp di module penjualan harian --- .../closings/dto/closingKeuangan.dto.go | 434 ++++++++++++++++-- internal/modules/closings/module.go | 4 +- .../closings/services/closing.service.go | 221 +-------- .../expense_realization.repository.go | 4 +- .../salesorder_delivery_product.repository.go | 3 +- .../repositories/recording.repository.go | 71 +++ .../repositories/purchase.repository.go | 12 + .../controllers/repport.controller.go | 24 +- .../repports/dto/repportMarketing.dto.go | 47 +- internal/modules/repports/module.go | 6 +- .../repports/services/repport.service.go | 65 ++- 11 files changed, 592 insertions(+), 299 deletions(-) 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 } From 096a446450b5a3223642e73e7f5fb4e30a3d5668 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 10:45:04 +0700 Subject: [PATCH 05/11] feat[BE]: update HPP calculations to use totalWeightProduced and totalActualPopulation --- .../closings/dto/closingKeuangan.dto.go | 71 +++++++++---------- .../closings/dto/closingOverhead.dto.go | 13 ++-- internal/modules/closings/module.go | 4 +- .../closings/services/closing.service.go | 21 +++++- 4 files changed, 62 insertions(+), 47 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index cf4b5b54..13e7c196 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -96,7 +96,7 @@ func getFlagLabel(flagType utils.FlagType) string { return "Pembelian " + string(flagType) } -func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightProduced, totalPopulation float64) []HppItem { flags := []utils.FlagType{ utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, @@ -125,7 +125,7 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe if isValid && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) items = append(items, HppItem{ Type: getFlagLabel(flagType), @@ -144,14 +144,14 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === -func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup { +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, 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) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) if budgetAmount > 0 || realizationAmount > 0 { items = append(items, HppItem{ @@ -165,7 +165,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced) if ekspedisiAmount > 0 { items = append(items, HppItem{ @@ -185,7 +185,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti // === HPP SUMMARY === -func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp { +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) SummaryHpp { // Budget: purchases + budgets purchaseTotal := sumPurchaseTotal(purchaseItems) budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) @@ -194,8 +194,8 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ // Realization: all expenses totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) - budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightProduced) return SummaryHpp{ Label: label, @@ -206,16 +206,16 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ } } -func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection { hppGroups := []HppGroup{ { GroupName: "HPP dan Pengeluaran", - Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation), + Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation), }, - ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation), + ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation), } - summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) return HppPurchasesSection{ Hpp: hppGroups, @@ -266,37 +266,33 @@ func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.M 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 - }) +func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { + // Calculate total cost using same logic as report penjualan: + // Total Cost = All Purchase Items + All BOP Expenses + purchaseAmount := sumPurchaseTotal(purchases) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + // Get BOP expenses (all expenses except ekspedisi) + bopAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + + totalCost := purchaseAmount + bopAmount + + rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + ToPLItem("Harga Pokok Penjualan (HPP)", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), } } -func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { +func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) return []PLItem{ ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), } } -func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { +func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) return []PLItem{ ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } @@ -348,7 +344,7 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec // === MAIN BUILDER === -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse { +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced float64) ReportResponse { var totalPopulation float64 var totalWeightSold float64 @@ -360,12 +356,13 @@ func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entiti totalWeightSold += delivery.TotalWeight } - hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + // Use totalWeightProduced for HPP calculation (not totalWeightSold) + hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold) - overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold) - ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold) + pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, totalPopulation, totalWeightProduced) + overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightProduced) + ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightProduced) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) diff --git a/internal/modules/closings/dto/closingOverhead.dto.go b/internal/modules/closings/dto/closingOverhead.dto.go index 95f3e10b..71975da1 100644 --- a/internal/modules/closings/dto/closingOverhead.dto.go +++ b/internal/modules/closings/dto/closingOverhead.dto.go @@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal return dto } -func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO { +func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { overheadsByNonstockID := make(map[uint]*OverheadDTO) latestDateByNonstockID := make(map[uint]string) @@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex for nonstockID, overhead := range overheadsByNonstockID { overhead.ActualDate = latestDateByNonstockID[nonstockID] - overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty) + + overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation) if overhead.ActualQuantity > 0 { overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity @@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex BudgetTotalAmount: totalBudgetAmount, ActualQuantity: totalActualQuantity, ActualTotalAmount: totalActualAmount, - CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty), + CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation), }, Overheads: overheadItems, } @@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 { return qty * price } -func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 { - if totalChickinQty > 0 { - return totalPrice / totalChickinQty +func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 { + if totalActualPopulation > 0 { + return totalPrice / totalActualPopulation } return 0 } diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index 494f2736..c89e6125 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" + rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" @@ -31,11 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) + recordingRepo := rRecording.NewRecordingRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, 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 29001149..1cb26948 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" + recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/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" @@ -47,9 +48,10 @@ type closingService struct { ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository PurchaseRepo purchaseRepository.PurchaseRepository + RecordingRepo recordingRepository.RecordingRepository } -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 { +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, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -62,6 +64,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, PurchaseRepo: purchaseRepo, + RecordingRepo: recordingRepo, } } @@ -379,7 +382,14 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove totalChickinQty += chickin.UsageQty } - result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty) + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + totalActualPopulation := totalChickinQty - totalDepletion + + result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) return &result, nil } @@ -435,7 +445,12 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins) + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) + } + + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced) return &report, nil } From f2df7f4847fdc81856ae03e9af6178813c19ce46 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 14:49:48 +0700 Subject: [PATCH 06/11] feat[BE]: add overhead and ekspedisi items to profit loss report; include total depletion in closing report calculation --- .../closings/dto/closingKeuangan.dto.go | 78 ++++++++++++------- .../closings/services/closing.service.go | 8 +- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 13e7c196..978c0b60 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -60,6 +60,8 @@ type PLSummaryGroup struct { type ProfitLossData struct { Penjualan []PLItem `json:"penjualan"` Pembelian []PLItem `json:"pembelian"` + Overhead PLItem `json:"overhead"` + Ekspedisi PLItem `json:"ekspedisi"` Summary PLSummaryGroup `json:"summary"` } @@ -167,15 +169,13 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced) - if ekspedisiAmount > 0 { - items = append(items, HppItem{ - Type: "Beban Ekspedisi", - Comparison: ToComparison( - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization - ), - }) - } + items = append(items, HppItem{ + Type: "Beban Ekspedisi", + Comparison: ToComparison( + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ), + }) return HppGroup{ GroupName: "HPP dan Bahan Baku", @@ -248,19 +248,28 @@ func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { } func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { + items := []PLItem{} + // Categorize deliveries by sales type based on Product flags categorized := categorizeDeliveriesBySalesType(deliveryProducts) - items := []PLItem{} + if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { + // For LAYING: show both Penjualan Ayam Besar and Penjualan Telur (even if 0) + ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) + telurAmount := sumDeliveriesByCategory(categorized["Penjualan Telur"]) - // Process each sales category - for salesType, deliveries := range categorized { - amount := sumDeliveriesByCategory(deliveries) + // Penjualan Ayam Besar + rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) - // Use totalPopulation and totalWeightSold for per-unit calculations - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) - - items = append(items, ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))) + // Penjualan Telur + rpPerBird, rpPerKg = calculatePerUnitMetrics(telurAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Telur", ToFinancialMetrics(rpPerBird, rpPerKg, telurAmount))) + } else { + // For GROWING: show only Penjualan Ayam Besar + ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) + rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) } return items @@ -278,7 +287,7 @@ func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.Proj rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Harga Pokok Penjualan (HPP)", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), + ToPLItem("Pembelian Sapronak", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), } } @@ -301,20 +310,21 @@ func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulatio func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) - totalOverhead, _ := sumPLItems(overheadItems) - totalEkspedisi, _ := sumPLItems(ekspedisiItems) + totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems) + totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems) grossProfit := totalPenjualan - totalPembelian grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird totalOtherExpenses := totalOverhead + totalEkspedisi + totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird netProfit := grossProfit - totalOtherExpenses - netProfitPerBird := grossProfitPerBird - 0.0 + netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird return PLSummaryGroup{ GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(0, 0, totalOtherExpenses)), + SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), } } @@ -322,9 +332,15 @@ func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiIt func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + // Get total overhead and ekspedisi as single items + totalOverhead := aggregatePLItems(overheadItems, "Pengeluaran Overhead") + totalEkspedisi := aggregatePLItems(ekspedisiItems, "Beban Ekspedisi") + return ProfitLossData{ Penjualan: penjualanItems, Pembelian: pembelianItems, + Overhead: totalOverhead, + Ekspedisi: totalEkspedisi, Summary: summary, } } @@ -335,6 +351,11 @@ func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedis } } +func aggregatePLItems(items []PLItem, label string) PLItem { + totalAmount, totalPerBird := sumPLItems(items) + return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount)) +} + func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { return ReportResponse{ HppPurchases: hppPurchases, @@ -342,9 +363,7 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -// === MAIN BUILDER === - -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced float64) ReportResponse { +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced, totalDepletion float64) ReportResponse { var totalPopulation float64 var totalWeightSold float64 @@ -356,13 +375,16 @@ func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entiti totalWeightSold += delivery.TotalWeight } + // Calculate actual population (chickin - depletion) for cost allocation + actualPopulation := totalPopulation - totalDepletion + // Use totalWeightProduced for HPP calculation (not totalWeightSold) hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, totalPopulation, totalWeightProduced) - overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightProduced) - ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightProduced) + pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, actualPopulation, totalWeightProduced) + overheadItems := ToOverheadItems(budgets, realizations, actualPopulation, totalWeightProduced) + ekspedisiItems := ToEkspedisiItems(realizations, actualPopulation, totalWeightProduced) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 1cb26948..84b14ace 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -450,7 +450,13 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced) + // Fetch depletion data to calculate actual population for cost allocation + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced, totalDepletion) return &report, nil } From 9e0b4be4dd60b66a3f64890f18d6911163c4c768 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 14:52:51 +0700 Subject: [PATCH 07/11] feat[BE]: add flags to product seeds for better categorization --- internal/database/seed/seeder.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 8da408ca..bb4090bb 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -588,6 +588,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamAfkir}, }, { Name: "Ayam Mati", @@ -596,6 +597,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamMati}, }, { Name: "Ayam Culling", @@ -604,6 +606,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamCulling}, }, { Name: "Telur Konsumsi Baik", @@ -612,6 +615,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurUtuh}, }, { Name: "Telur Pecah", @@ -620,6 +624,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurPecah}, }, { Name: "281 SPECIAL STARTER", @@ -632,6 +637,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, + { + Name: "Ayam Layer", + Brand: "-", + Sku: "LYR0001", + Uom: "Ekor", + Category: "Pullet", + Price: 20000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagLayer}, + }, } for _, seed := range seeds { From c95f90f0b9d1060059f2ec5315a614a323406f8c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 15:03:37 +0700 Subject: [PATCH 08/11] Refactor[BE]: refactor expense category handling to use constants for BOP and NON-BOP --- .../expenses/services/expense.service.go | 16 ++++++++-------- internal/utils/constant.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index dbfb00c2..24ba4f2e 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -213,7 +213,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen var projectFlockKandangId *uint64 - if req.Category == "BOP" { + if req.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -230,10 +230,10 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen nonstockId := costItem.NonstockID var kandangId *uint64 - if req.Category == "NON-BOP" { + if req.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if req.Category == "BOP" { + } else if req.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } @@ -385,7 +385,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if categoryChanged { - if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { + if currentExpense.Category == string(utils.ExpenseCategoryBOP) && newCategory == string(utils.ExpenseCategoryNonBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -400,7 +400,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") } } - } else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { + } else if currentExpense.Category == string(utils.ExpenseCategoryNonBOP) && newCategory == string(utils.ExpenseCategoryBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -457,7 +457,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) for _, expenseNonstock := range *req.ExpenseNonstocks { var projectFlockKandangId *uint64 - if updatedExpense.Category == "BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -480,10 +480,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } var kandangId *uint64 - if updatedExpense.Category == "NON-BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if updatedExpense.Category == "BOP" { + } else if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d4f6ec02..85b33f9b 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -135,6 +135,17 @@ const ( SupplierCategorySapronak SupplierCategory = "SAPRONAK" ) +// ------------------------------------------------------------------- +// ExpenseCategory +// ------------------------------------------------------------------- + +type ExpenseCategory string + +const ( + ExpenseCategoryBOP ExpenseCategory = "BOP" + ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP" +) + // ------------------------------------------------------------------- // Kandang Status // ------------------------------------------------------------------- @@ -429,6 +440,14 @@ func IsValidSupplierCategory(v string) bool { return false } +func IsValidExpenseCategory(v string) bool { + switch ExpenseCategory(v) { + case ExpenseCategoryBOP, ExpenseCategoryNonBOP: + return true + } + return false +} + // example use // Recording helper From e551995c66c86ebeb3de12bee755f2df975826c9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 17:56:18 +0700 Subject: [PATCH 09/11] feat[BE-384]: enhance reporting by adding chickin quantity and egg production weight calculations; refactor HPP calculations to consider product categories --- .../salesorder_delivery_product.repository.go | 1 + .../project_chickin.repository.go | 12 ++ .../repositories/recording.repository.go | 22 ++- .../repports/dto/repportMarketing.dto.go | 100 ++++++++++---- internal/modules/repports/module.go | 4 +- .../repports/services/repport.service.go | 128 +++++++----------- 6 files changed, 157 insertions(+), 110 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index b908681e..ba2c1133 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -90,6 +90,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). + Preload("ProductWarehouse.Product.Flags"). Preload("ProductWarehouse.Warehouse"). Preload("ProductWarehouse.ProjectFlockKandang"). Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock") diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index bef062f5..43cafaac 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -15,6 +15,7 @@ type ProjectChickinRepository interface { GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) } type ChickinRepositoryImpl struct { @@ -90,3 +91,14 @@ func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(c } return total, nil } + +func (r *ChickinRepositoryImpl) 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 +} diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 4a7e627c..85c9a7fe 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -48,6 +48,7 @@ type RecordingRepository interface { 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) + GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) } type RecordingRepositoryImpl struct { @@ -371,28 +372,23 @@ func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx 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 @@ -434,6 +430,22 @@ func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context return result, err } +func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_eggs"). + Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000"). + Joins("JOIN recordings ON recordings.id = recording_eggs.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 nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index deadf3b8..9c026590 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,14 +1,15 @@ package dto import ( - "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type RepportMarketingItemDTO struct { @@ -45,7 +46,7 @@ type RepportMarketingResponseDTO struct { Total *Summary `json:"total,omitempty"` } -func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO { soDate := time.Time{} agingDays := 0 if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { @@ -58,11 +59,17 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK realizationDate = *mdp.DeliveryDate } - doNumber := generateDoNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) + doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) totalWeightKg := mdp.Qty * mdp.AvgWeight salesAmount := totalWeightKg * mdp.UnitPrice - hppAmount := totalWeightKg * hppPricePerKg + + var hpp float64 + var hppAmount float64 + if isProductEligibleForHpp(mdp, category) { + hpp = hppPricePerKg + hppAmount = totalWeightKg * hppPricePerKg + } item := RepportMarketingItemDTO{ ID: int(mdp.Id), @@ -70,12 +77,12 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK RealizationDate: realizationDate, AgingDays: agingDays, DoNumber: doNumber, - MarketingType: "ayam", + MarketingType: getMarketingType(mdp), Qty: mdp.Qty, AverageWeightKg: mdp.AvgWeight, TotalWeightKg: totalWeightKg, SalesPricePerKg: mdp.UnitPrice, - HppPricePerKg: hppPricePerKg, + HppPricePerKg: hpp, SalesAmount: salesAmount, HppAmount: hppAmount, } @@ -105,10 +112,10 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK return item } -func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO { items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { - items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg)) + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category)) } return items } @@ -117,23 +124,72 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { hppPerKg := float64(0) + category := "" if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { hppPerKg = hpp } + category = projectFlockKandang.ProjectFlock.Category } - items = append(items, ToRepportMarketingItemDTO(mdp, hppPerKg)) + + item := ToRepportMarketingItemDTO(mdp, hppPerKg, category) + items = append(items, item) } return items } -func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { +func getMarketingType(mdp entity.MarketingDeliveryProduct) string { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if hasAyam { + return "ayam" + } + if hasTelur { + return "telur" + } + return "trading" +} + +func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) { + if len(flags) == 0 { + return false, false + } + + for _, flag := range flags { + ft := utils.FlagType(flag.Name) + + if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati || + ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer { + hasAyam = true + } + + if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah || + ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak { + hasTelur = true + } + } + + return hasAyam, hasTelur +} + +func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + return hasAyam + } + + return hasAyam || hasTelur +} + +func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary { if len(mdps) == 0 { return nil } totalQty := 0 totalWeightKg := 0.0 + totalEligibleWeightKg := 0.0 totalSalesAmount := int64(0) totalHppAmount := int64(0) @@ -142,12 +198,16 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S totalQty += int(mdp.Qty) totalWeightKg += calculatedTotalWeight totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) - totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + + if isProductEligibleForHpp(mdp, category) { + totalEligibleWeightKg += calculatedTotalWeight + totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + } } totalHppPricePerKg := float64(0) - if totalWeightKg > 0 { - totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + if totalEligibleWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg } return &Summary{ @@ -159,14 +219,6 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S } } -func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string { - dateStr := "" - if deliveryDate != nil { - dateStr = deliveryDate.Format("20060102") - } - return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) -} - func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { if len(items) == 0 { return nil @@ -198,9 +250,9 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { } } -func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { - items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) - total := ToSummary(mdps, hppPricePerKg) +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category) + total := ToSummary(mdps, hppPricePerKg, category) return RepportMarketingResponseDTO{ Items: items, diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index f347ab69..95d77dc1 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -11,6 +11,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" ) @@ -22,11 +23,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) purchaseRepository := purchaseRepo.NewPurchaseRepository(db) + chickinRepository := chickinRepo.NewChickinRepository(db) recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 5458a28d..7513cbb1 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -3,7 +3,6 @@ package service import ( "context" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -12,6 +11,7 @@ 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" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" @@ -32,17 +32,19 @@ type repportService struct { ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository + ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, + ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, } @@ -98,74 +100,63 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) - hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) - items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) + projectFlockIDMap := make(map[uint]bool) + hppMap := make(map[uint]float64) + for _, dp := range deliveryProducts { + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + projectFlockID := projectFlockKandang.ProjectFlockId + if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] { + projectFlockIDMap[projectFlockID] = true + + category := projectFlockKandang.ProjectFlock.Category + hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category) + hppMap[projectFlockID] = hppPerKg + } + } + } + + items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) return items, total, nil } -func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint { - projectFlockIDMap := make(map[uint]bool) - projectFlockIDs := make([]uint, 0) - - for _, dp := range deliveryProducts { - if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] { - projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId) - projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true - } - } - } - - return projectFlockIDs -} - -func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { - hppMap := make(map[uint]float64) - for _, projectFlockID := range projectFlockIDs { - category := s.getProjectFlockCategory(deliveryProducts, projectFlockID) - hppPerKg := s.calculateHppByCategory(ctx, category, projectFlockID, deliveryProducts) - hppMap[projectFlockID] = hppPerKg - } - return hppMap -} - -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: +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { + totalCost := s.getTotalProjectCost(ctx, projectFlockID) + if totalCost == 0 { return 0 } -} -func (s *repportService) getProjectFlockCategory(deliveryProducts []entity.MarketingDeliveryProduct, projectFlockID uint) string { - for _, dp := range deliveryProducts { - if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if projectFlockKandang.ProjectFlockId == projectFlockID { - return projectFlockKandang.ProjectFlock.Category - } - } + chickinQty, _ := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + depletion, _ := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + avgWeight, _ := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + + var totalWeight float64 + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + totalWeight = (chickinQty - depletion) * avgWeight + } else { + eggWeight, _ := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + totalWeight = (chickinQty-depletion)*avgWeight + eggWeight } - return "" + + if totalWeight == 0 { + return 0 + } + return totalCost / totalWeight } -func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { +func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { if projectFlockID == 0 { return 0 } - purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) + purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) } - costPurchase := float64(0) - for _, item := range purchaseItems { - costPurchase += item.TotalPrice + cost := float64(0) + for _, p := range purchases { + cost += p.TotalPrice } realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) @@ -173,34 +164,11 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc s.Log.Warnf("GetByProjectFlockID error: %v", err) } - costBop := float64(0) - for _, realization := range realizations { - cost := realization.Price * realization.Qty - category := "" - if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil { - category = realization.ExpenseNonstock.Expense.Category - } - - if category == "BOP" { - costBop += cost + for _, r := range realizations { + if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && + r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { + cost += r.Price * r.Qty } } - - totalActualCost := costPurchase + costBop - - if totalActualCost == 0 { - return 0 - } - - totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID) - if err != nil { - s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) - } - - if totalWeightProduced == 0 { - return 0 - } - - hppPerKg := totalActualCost / totalWeightProduced - return hppPerKg + return cost } From fa6d82b79a0aca02c8a122736ba25f4f33297e3e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 19 Dec 2025 08:30:05 +0700 Subject: [PATCH 10/11] feat[BE-384]: enhance closing reports by introducing calculation context and improving data handling; refactor related functions for better clarity and maintainability --- .../closings/dto/closingKeuangan.dto.go | 305 ++++++++++-------- .../closings/dto/closingMarketing.dto.go | 20 +- .../closings/services/closing.service.go | 15 +- .../chickins/services/chickin.service.go | 29 +- .../services/project_flock_kandang.service.go | 15 +- .../repositories/recording.repository.go | 2 +- .../repositories/purchase.repository.go | 26 +- .../repports/services/repport.service.go | 41 ++- 8 files changed, 286 insertions(+), 167 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 978c0b60..90dda2a9 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,13 +1,58 @@ package dto import ( + "slices" "strings" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) +// === CONSTANTS === +const ( + HPPGroupPengeluaran = "HPP dan Pengeluaran" + HPPGroupBahanBaku = "HPP dan Bahan Baku" + HPPLabelOverhead = "Pengeluaran Overhead" + HPPLabelEkspedisi = "Beban Ekspedisi" + HPPSummaryLabel = "HPP" + + PLSalesTypeChicken = "Penjualan Ayam Besar" + PLSalesTypeEgg = "Penjualan Telur" + + PLItemTypeSapronak = "Pembelian Sapronak" + PLItemTypeOverhead = "Pengeluaran Overhead" + PLItemTypeEkspedisi = "Beban Ekspedisi" + + PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO" + PLSummaryLabelSubTotal = "SUB TOTAL" + PLSummaryLabelNetProfit = "LABA RUGI NETTO" + + PurchaseLabelPrefix = "Pembelian " +) + +// === CONTEXT STRUCTS === + +type CalculationContext struct { + TotalPopulation float64 + TotalWeightProduced float64 + TotalDepletion float64 + TotalWeightSold float64 + ActualPopulation float64 +} + +type ClosingKeuanganInput struct { + ProjectFlockCategory string + PurchaseItems []entities.PurchaseItem + Budgets []entities.ProjectBudget + Realizations []entities.ExpenseRealization + DeliveryProducts []entities.MarketingDeliveryProduct + Chickins []entities.ProjectChickin + TotalWeightProduced float64 + TotalDepletion float64 +} + // === BASE METRICS === + type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` @@ -20,6 +65,7 @@ type Comparison struct { } // === HPP PURCHASES PACKAGE === + type HppItem struct { Type string `json:"type"` Comparison @@ -41,6 +87,7 @@ type HppPurchasesSection struct { } // === PROFIT LOSS PACKAGE === + type PLItem struct { Type string `json:"type"` FinancialMetrics @@ -70,6 +117,7 @@ type ProfitLossSection struct { } // === RESPONSE DTO (ROOT) === + type ReportResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"` ProfitLoss ProfitLossSection `json:"profit_loss"` @@ -95,10 +143,10 @@ func ToComparison(budgeting, realization FinancialMetrics) Comparison { // === HPP PENGELUARAN (from Purchase Items) === func getFlagLabel(flagType utils.FlagType) string { - return "Pembelian " + string(flagType) + return PurchaseLabelPrefix + string(flagType) } -func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightProduced, totalPopulation float64) []HppItem { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem { flags := []utils.FlagType{ utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, @@ -116,24 +164,15 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe 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] { + if slices.Contains(flags, flagType) && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced) items = append(items, HppItem{ Type: getFlagLabel(flagType), Comparison: ToComparison( ToFinancialMetrics(rpPerBird, rpPerKg, amount), - ToFinancialMetrics(rpPerBird, rpPerKg, amount), // Same for purchase + ToFinancialMetrics(rpPerBird, rpPerKg, amount), ), }) seenFlags[flagType] = true @@ -146,56 +185,61 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === -func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppGroup { - items := []HppItem{} +func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem { + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) - // 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, totalWeightProduced) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) - - if budgetAmount > 0 || realizationAmount > 0 { - items = append(items, HppItem{ - Type: "Pengeluaran Overhead", - Comparison: ToComparison( - ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), - ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), - ), - }) + return HppItem{ + Type: HPPLabelOverhead, + 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, totalWeightProduced) +func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem { + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) - items = append(items, HppItem{ - Type: "Beban Ekspedisi", + return HppItem{ + Type: HPPLabelEkspedisi, Comparison: ToComparison( ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), ), - }) + } +} + +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup { + items := []HppItem{} + + budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + realizationAmount := getOperationalExpenses(realizations) + + if budgetAmount > 0 || realizationAmount > 0 { + items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx)) + } + + ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx)) return HppGroup{ - GroupName: "HPP dan Bahan Baku", + GroupName: HPPGroupBahanBaku, Data: items, } } // === HPP SUMMARY === -func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) SummaryHpp { - // Budget: purchases + budgets +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp { 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, totalWeightProduced) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightProduced) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) return SummaryHpp{ Label: label, @@ -206,16 +250,16 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ } } -func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection { hppGroups := []HppGroup{ { - GroupName: "HPP dan Pengeluaran", - Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation), + GroupName: HPPGroupPengeluaran, + Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx), }, - ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation), + ToHppBahanBakuGroup(budgets, realizations, ctx), } - summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) + summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx) return HppPurchasesSection{ Hpp: hppGroups, @@ -239,6 +283,11 @@ func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { } } +func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced) + return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { for _, item := range items { totalAmount += item.Amount @@ -247,63 +296,51 @@ func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { return } -func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { +func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold) + return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + +func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem { items := []PLItem{} - // Categorize deliveries by sales type based on Product flags categorized := categorizeDeliveriesBySalesType(deliveryProducts) if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { - // For LAYING: show both Penjualan Ayam Besar and Penjualan Telur (even if 0) - ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) - telurAmount := sumDeliveriesByCategory(categorized["Penjualan Telur"]) + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg]) - // Penjualan Ayam Besar - rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) - - // Penjualan Telur - rpPerBird, rpPerKg = calculatePerUnitMetrics(telurAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Telur", ToFinancialMetrics(rpPerBird, rpPerKg, telurAmount))) + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) + items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx)) } else { - // For GROWING: show only Penjualan Ayam Besar - ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) - rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) } return items } -func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { - // Calculate total cost using same logic as report penjualan: - // Total Cost = All Purchase Items + All BOP Expenses +func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { purchaseAmount := sumPurchaseTotal(purchases) - - // Get BOP expenses (all expenses except ekspedisi) - bopAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - + bopAmount := getOperationalExpenses(realizations) totalCost := purchaseAmount + bopAmount - rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Pembelian Sapronak", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), + createPLItemWithMetrics(PLItemTypeSapronak, totalCost, ctx), } } -func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { - realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) +func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { + realizationAmount := getOperationalExpenses(realizations) return []PLItem{ - ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), + createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx), } } -func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { +func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx), } } @@ -323,18 +360,17 @@ func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiIt netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird return PLSummaryGroup{ - GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), - NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), + GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), + NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)), } } func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) - // Get total overhead and ekspedisi as single items - totalOverhead := aggregatePLItems(overheadItems, "Pengeluaran Overhead") - totalEkspedisi := aggregatePLItems(ekspedisiItems, "Beban Ekspedisi") + totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead) + totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi) return ProfitLossData{ Penjualan: penjualanItems, @@ -363,28 +399,31 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced, totalDepletion float64) ReportResponse { +func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse { var totalPopulation float64 var totalWeightSold float64 - for _, chickin := range chickins { + for _, chickin := range input.Chickins { totalPopulation += chickin.UsageQty } - for _, delivery := range deliveryProducts { + for _, delivery := range input.DeliveryProducts { totalWeightSold += delivery.TotalWeight } - // Calculate actual population (chickin - depletion) for cost allocation - actualPopulation := totalPopulation - totalDepletion + ctx := CalculationContext{ + TotalPopulation: totalPopulation, + TotalWeightProduced: input.TotalWeightProduced, + TotalDepletion: input.TotalDepletion, + TotalWeightSold: totalWeightSold, + ActualPopulation: totalPopulation - input.TotalDepletion, + } - // Use totalWeightProduced for HPP calculation (not totalWeightSold) - hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) - - penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, actualPopulation, totalWeightProduced) - overheadItems := ToOverheadItems(budgets, realizations, actualPopulation, totalWeightProduced) - ekspedisiItems := ToEkspedisiItems(realizations, actualPopulation, totalWeightProduced) + hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, ctx) + penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx) + pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx) + overheadItems := ToOverheadItems(input.Realizations, ctx) + ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) @@ -402,17 +441,21 @@ func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) ( return rpPerBird, rpPerKg } +func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool { + for _, flag := range flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false +} + 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 + return hasProductFlag(item.Product.Flags, flagType) } } @@ -421,13 +464,7 @@ func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.Exp 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 + return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType) } } @@ -438,46 +475,38 @@ func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.Expense } } -func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { +func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 { amount := 0.0 - for i := range purchases { - if filter(&purchases[i]) { - amount += purchases[i].TotalPrice + for i := range items { + if filter(&items[i]) { + amount += extractor(&items[i]) } } return amount } +func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { + return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter) +} + 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 + return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true }) } 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 + return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter) } 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 + return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter) +} + +func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 { + return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) } func isChickenProductFlag(flagType utils.FlagType) bool { @@ -500,21 +529,21 @@ func isEggProductFlag(flagType utils.FlagType) bool { func getSalesTypeFromProductFlags(product *entities.Product) string { if product == nil || len(product.Flags) == 0 { - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } for _, flag := range product.Flags { flagType := utils.FlagType(strings.ToUpper(flag.Name)) if isEggProductFlag(flagType) { - return "Penjualan Telur" + return PLSalesTypeEgg } if isChickenProductFlag(flagType) { - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } } - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index ea0ddb81..8c904561 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -35,8 +35,7 @@ type PenjualanRealisasiResponseDTO struct { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { - // todo: usia ayam masih dummy - age := 0 + age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -101,3 +100,20 @@ func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int } return 0 } + +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { + if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0 + } + + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) + ageInWeeks := ageInDays / 7 + return ageInWeeks +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 84b14ace..acb75871 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -137,6 +137,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit Preload("MarketingProduct.ProductWarehouse.Warehouse"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins"). Preload("MarketingProduct.Marketing"). Preload("MarketingProduct.Marketing.Customer"). Order("marketing_delivery_products.delivery_date DESC") @@ -450,13 +451,23 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - // Fetch depletion data to calculate actual population for cost allocation totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced, totalDepletion) + input := dto.ClosingKeuanganInput{ + ProjectFlockCategory: projectFlock.Category, + PurchaseItems: purchaseItems, + Budgets: budgets, + Realizations: realizations, + DeliveryProducts: deliveryProducts, + Chickins: chickins, + TotalWeightProduced: totalWeightProduced, + TotalDepletion: totalDepletion, + } + + report := dto.ToClosingKeuanganReport(input) return &report, nil } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index cb816431..b8eefa49 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -143,6 +143,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) } + if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) + } + chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) @@ -450,7 +454,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -466,7 +471,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -538,11 +544,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { +func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId) if err == nil && len(products) > 0 { - return &products[0], nil + existingPW := &products[0] + // Update project_flock_kandang_id if not already set + if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil { + existingPW.ProjectFlockKandangId = projectFlockKandangId + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil { + return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err) + } + } + return existingPW, nil } product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) @@ -554,9 +568,10 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId } newPW := &entity.ProductWarehouse{ - ProductId: product.Id, - WarehouseId: warehouseId, - Quantity: 0, + ProductId: product.Id, + WarehouseId: warehouseId, + ProjectFlockKandangId: projectFlockKandangId, + Quantity: 0, // CreatedBy: actorID, } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 7effdc35..cf2d87ee 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -190,13 +190,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project result := make(map[uint]float64) for _, pw := range products { - availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) - if err != nil { - s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) - } - if availableQty > 0 { - result[pw.Id] = availableQty + if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id { + availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) + } + + if availableQty > 0 { + result[pw.Id] = availableQty + } } } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 85c9a7fe..6e362ba7 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -425,7 +425,7 @@ func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context 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)"). + Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID). Scan(&result).Error return result, err } diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 2f9b2774..fc599877 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -26,6 +26,7 @@ type PurchaseRepository interface { 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) + GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } type PurchaseRepositoryImpl struct { @@ -291,13 +292,34 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, } func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + + return r.GetItemsByWarehouseKandang(ctx, projectFlockID) +} + +func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { var items []entity.PurchaseItem + + var kandangIDs []uint err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Pluck("kandang_id", &kandangIDs).Error + + if err != nil { + return nil, err + } + + if len(kandangIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + 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). + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.kandang_id IN ?", kandangIDs). Find(&items).Error + return items, err } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 7513cbb1..ee00d0d8 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -123,25 +123,42 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { totalCost := s.getTotalProjectCost(ctx, projectFlockID) if totalCost == 0 { + s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID) return 0 } - chickinQty, _ := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) - depletion, _ := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) - avgWeight, _ := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err) + } + + depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err) + } + + avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err) + } var totalWeight float64 if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { totalWeight = (chickinQty - depletion) * avgWeight } else { - eggWeight, _ := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err) + } totalWeight = (chickinQty-depletion)*avgWeight + eggWeight } if totalWeight == 0 { return 0 } - return totalCost / totalWeight + + hppPricePerKg := totalCost / totalWeight + return hppPricePerKg } func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { @@ -151,24 +168,30 @@ func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { - s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) + s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) + return 0 } cost := float64(0) + purchaseCost := float64(0) for _, p := range purchases { - cost += p.TotalPrice + purchaseCost += p.TotalPrice } + cost += purchaseCost realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) if err != nil { - s.Log.Warnf("GetByProjectFlockID error: %v", err) + s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) } + bopCost := float64(0) for _, r := range realizations { if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { - cost += r.Price * r.Qty + bopCost += r.Price * r.Qty } } + cost += bopCost + return cost } From ef117e66d16b0f87214a274691658cb71061c6e0 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 22 Dec 2025 10:03:32 +0700 Subject: [PATCH 11/11] add permission deliveryorder and sales order --- internal/middleware/permissions.go | 11 ++++++++++- internal/modules/marketing/route.go | 16 ++++++---------- .../modules/production/transfer_layings/route.go | 14 +++++++------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index e715aae9..9e2b5e5e 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -73,10 +73,19 @@ const ( P_TransferCreateOne = "lti.inventory.transfer.create" ) +const ( + P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list" + P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail" + P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create" + P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update" + P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete" + P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve" + P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty" +) + const ( P_DeliveryGetAll = "lti.marketing.delivery_order.list" P_DeliveryGetOne = "lti.marketing.delivery_order.detail" - P_DeliveryCreateOne = "lti.marketing.delivery_order.create" P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" P_SalesOrderDelete = "lti.marketing.sales_order.delete" P_SalesOrderApproval = "lti.marketing.sales_order.approve" diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 75ecc0f6..139d1ee9 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -16,16 +16,12 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde route := router.Group("/marketing") route.Use(m.Auth(userService)) - route.Get("/", deliveryOrdersCtrl.GetAll) - route.Get("/:id", deliveryOrdersCtrl.GetOne) - route.Delete("/:id", salesOrdersCtrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) + route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) - route.Post("/sales-orders", salesOrdersCtrl.CreateOne) - route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne) - route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval) + route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) + route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne) + route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval) - route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll) - route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne) - route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne) - route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne) } diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 868454c5..8f7a62c0 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -21,11 +21,11 @@ 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("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) - route.Get("/project-flocks/:project_flock_id/available-qty", 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) }