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 package dto
import (
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === BASE METRICS === // === BASE METRICS ===
type FinancialMetrics struct { type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"` RpPerBird float64 `json:"rp_per_bird"`
@@ -28,9 +35,7 @@ type SummaryHpp struct {
Comparison Comparison
} }
// Ini adalah struct mandiri untuk bagian HPP Purchases
type HppPurchasesSection struct { type HppPurchasesSection struct {
Title string `json:"title"`
Hpp []HppGroup `json:"hpp"` Hpp []HppGroup `json:"hpp"`
SummaryHpp SummaryHpp `json:"summary_hpp"` SummaryHpp SummaryHpp `json:"summary_hpp"`
} }
@@ -58,14 +63,11 @@ type ProfitLossData struct {
Summary PLSummaryGroup `json:"summary"` Summary PLSummaryGroup `json:"summary"`
} }
// Ini adalah struct mandiri untuk bagian Profit Loss
type ProfitLossSection struct { type ProfitLossSection struct {
Title string `json:"title"` Data ProfitLossData `json:"data"`
Data ProfitLossData `json:"data"`
} }
// === RESPONSE DTO (ROOT) === // === RESPONSE DTO (ROOT) ===
// Sekarang Root-nya terlihat sangat bersih dan tidak "janggal" lagi
type ReportResponse struct { type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"` HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"` ProfitLoss ProfitLossSection `json:"profit_loss"`
@@ -73,7 +75,6 @@ type ReportResponse struct {
// === MAPPER FUNCTIONS === // === MAPPER FUNCTIONS ===
// FinancialMetrics Mappers
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{ return FinancialMetrics{
RpPerBird: rpPerBird, RpPerBird: rpPerBird,
@@ -82,7 +83,6 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
} }
} }
// Comparison Mappers
func ToComparison(budgeting, realization FinancialMetrics) Comparison { func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{ return Comparison{
Budgeting: budgeting, Budgeting: budgeting,
@@ -90,40 +90,141 @@ func ToComparison(budgeting, realization FinancialMetrics) Comparison {
} }
} }
// HppItem Mappers // === HPP PENGELUARAN (from Purchase Items) ===
func ToHppItem(itemType string, comparison Comparison) HppItem {
return HppItem{ func getFlagLabel(flagType utils.FlagType) string {
Type: itemType, return "Pembelian " + string(flagType)
Comparison: comparison,
}
} }
// HppGroup Mappers func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem {
func ToHppGroup(groupName string, items []HppItem) HppGroup { 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{ return HppGroup{
GroupName: groupName, GroupName: "HPP dan Bahan Baku",
Data: items, Data: items,
} }
} }
// SummaryHpp Mappers // === HPP SUMMARY ===
func ToSummaryHpp(label string, comparison Comparison) SummaryHpp {
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{ return SummaryHpp{
Label: label, Label: label,
Comparison: comparison, Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
} }
} }
// HppPurchasesSection Mappers func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection {
func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) 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{ return HppPurchasesSection{
Title: title,
Hpp: hppGroups, Hpp: hppGroups,
SummaryHpp: summaryHpp, SummaryHpp: summaryHpp,
} }
} }
// PLItem Mappers // === PROFIT & LOSS ===
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
return PLItem{ return PLItem{
Type: itemType, Type: itemType,
@@ -131,7 +232,6 @@ func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
} }
} }
// PLSummaryItem Mappers
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
return PLSummaryItem{ return PLSummaryItem{
Label: label, Label: label,
@@ -139,33 +239,106 @@ func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
} }
} }
// PLSummaryGroup Mappers func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { for _, item := range items {
return PLSummaryGroup{ totalAmount += item.Amount
GrossProfit: grossProfit, totalPerBird += item.RpPerBird
SubTotal: subTotal, }
NetProfit: netProfit, 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 ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem {
func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { 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{ return ProfitLossData{
Penjualan: penjualan, Penjualan: penjualanItems,
Pembelian: pembelian, Pembelian: pembelianItems,
Summary: summary, Summary: summary,
} }
} }
// ProfitLossSection Mappers func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection {
return ProfitLossSection{ return ProfitLossSection{
Title: title, Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
Data: data,
} }
} }
// ReportResponse Mappers
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
return ReportResponse{ return ReportResponse{
HppPurchases: hppPurchases, HppPurchases: hppPurchases,
@@ -173,14 +346,175 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec
} }
} }
// Helper function to create a complete financial report // === MAIN BUILDER ===
func BuildFinancialReport(
hppGroups []HppGroup, func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse {
summaryHpp SummaryHpp, var totalPopulation float64
penjualan, pembelian []PLItem, var totalWeightSold float64
plSummary PLSummaryGroup,
) ReportResponse { for _, chickin := range chickins {
hppSection := ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) totalPopulation += chickin.UsageQty
plSection := ToProfitLossSection("Laporan Laba Rugi", ToProfitLossData(penjualan, pembelian, plSummary)) }
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) 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" rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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" 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" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" 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) marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) 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) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -15,6 +15,7 @@ import (
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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" 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" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -45,9 +46,10 @@ type closingService struct {
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository 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{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -59,6 +61,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ExpenseRealizationRepo: expenseRealizationRepo, ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo, ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo, 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") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
_, err := s.Repository.GetByID(c.Context(), projectFlockID, nil) if err := commonSvc.EnsureRelations(c.Context(),
if errors.Is(err, gorm.ErrRecordNotFound) { commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") _, 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 { 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get budgets for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") 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) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { 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") 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") Preload("MarketingProduct.ProductWarehouse.Product")
}) })
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
} }
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
} }
var totalPopulation float64 report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins)
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 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)),
)
}
@@ -44,6 +44,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
Preload("ExpenseNonstock"). Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Nonstock"). Preload("ExpenseNonstock.Nonstock").
Preload("ExpenseNonstock.Nonstock.Uom"). Preload("ExpenseNonstock.Nonstock.Uom").
Preload("ExpenseNonstock.Nonstock.Flags").
Preload("ExpenseNonstock.Expense"). Preload("ExpenseNonstock.Expense").
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_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("Expense.Supplier").
Preload("Kandang"). Preload("Kandang").
Preload("Kandang.Location"). 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 expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
@@ -91,7 +91,8 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Preload("ProductWarehouse"). Preload("ProductWarehouse").
Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse"). 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 marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
@@ -45,6 +45,9 @@ type RecordingRepository interface {
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, 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 { type RecordingRepositoryImpl struct {
@@ -363,6 +366,74 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint
return weight, true, nil 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 { func nextRecordingDay(days []int) int {
if len(days) == 0 { if len(days) == 0 {
return 1 return 1
@@ -25,6 +25,7 @@ type PurchaseRepository interface {
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
} }
type PurchaseRepositoryImpl struct { type PurchaseRepositoryImpl struct {
@@ -289,6 +290,17 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB,
return count > 0, nil 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) { func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) { if !strings.HasPrefix(value, prefix) {
return 0, false return 0, false
@@ -96,28 +96,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
return err return err
} }
// Calculate total summary from result items
var total *dto.Summary total := dto.ToSummaryFromDTOItems(result)
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). return ctx.Status(fiber.StatusOK).
JSON(MarketingReportResponse{ JSON(MarketingReportResponse{
@@ -50,7 +50,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
agingDays := 0 agingDays := 0
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
soDate = mdp.MarketingProduct.Marketing.SoDate soDate = mdp.MarketingProduct.Marketing.SoDate
agingDays = int(time.Now().Sub(soDate).Hours() / 24) agingDays = int(time.Since(soDate).Hours() / 24)
} }
realizationDate := time.Time{} realizationDate := time.Time{}
@@ -113,6 +113,20 @@ func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPrice
return items 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 { func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary {
if len(mdps) == 0 { if len(mdps) == 0 {
return nil return nil
@@ -153,6 +167,37 @@ func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) 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 { func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO {
items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg)
total := ToSummary(mdps, hppPricePerKg) total := ToSummary(mdps, hppPricePerKg)
+5 -1
View File
@@ -11,6 +11,8 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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{} type RepportModule struct{}
@@ -19,10 +21,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
purchaseRepository := purchaseRepo.NewPurchaseRepository(db)
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository) approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc)
RepportRoutes(router, repportService) RepportRoutes(router, repportService)
} }
@@ -12,6 +12,8 @@ import (
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -29,15 +31,19 @@ type repportService struct {
Validate *validator.Validate Validate *validator.Validate
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService 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{ return &repportService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
ExpenseRealizationRepo: expenseRealizationRepo, ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo, MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
} }
} }
@@ -94,7 +100,7 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts)
hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts)
items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
return items, total, nil 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 { func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 {
hppMap := make(map[uint]float64) hppMap := make(map[uint]float64)
for _, projectFlockID := range projectFlockIDs { 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 hppMap[projectFlockID] = hppPerKg
} }
return hppMap return hppMap
} }
func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO { func (s *repportService) calculateHppByCategory(ctx context.Context, category string, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 {
items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts)) 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 { for _, dp := range deliveryProducts {
hppPerKg := float64(0)
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { if projectFlockKandang.ProjectFlockId == projectFlockID {
hppPerKg = hpp 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 { 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 return 0
} }
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID)
if err != nil { if err != nil {
return 0 s.Log.Warnf("GetItemsByProjectFlockID error: %v", err)
} }
if len(realizations) == 0 { costPurchase := float64(0)
return 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) costBop := float64(0)
for _, realization := range realizations { for _, realization := range realizations {
cost := realization.Price * realization.Qty cost := realization.Price * realization.Qty
category := "" category := ""
@@ -166,24 +186,21 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc
} }
} }
totalActualCost := costBop totalActualCost := costPurchase + costBop
if totalActualCost == 0 { if totalActualCost == 0 {
return 0 return 0
} }
totalWeightSold := float64(0) totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID)
for _, dp := range deliveryProducts { if err != nil {
if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID {
totalWeightSold += dp.TotalWeight
}
} }
if totalWeightSold == 0 { if totalWeightProduced == 0 {
return 0 return 0
} }
hppPerKg := totalActualCost / totalWeightSold hppPerKg := totalActualCost / totalWeightProduced
return hppPerKg return hppPerKg
} }