feat[BE]: membetulkan perhitungan hpp di module penjualan harian

This commit is contained in:
aguhh18
2025-12-18 09:58:31 +07:00
parent a7069a2e50
commit 1b23861656
11 changed files with 592 additions and 299 deletions
@@ -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
}
+3 -1
View File
@@ -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)
@@ -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)),
)
}