Files
lti-api/internal/modules/closings/dto/closingKeuangan.dto.go
T

540 lines
18 KiB
Go

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"`
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
}
type HppPurchasesSection struct {
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"`
Overhead PLItem `json:"overhead"`
Ekspedisi PLItem `json:"ekspedisi"`
Summary PLSummaryGroup `json:"summary"`
}
type ProfitLossSection struct {
Data ProfitLossData `json:"data"`
}
// === RESPONSE DTO (ROOT) ===
type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === MAPPER FUNCTIONS ===
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{
Budgeting: budgeting,
Realization: realization,
}
}
// === HPP PENGELUARAN (from Purchase Items) ===
func getFlagLabel(flagType utils.FlagType) string {
return "Pembelian " + string(flagType)
}
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,
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, totalWeightProduced)
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, 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, 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),
),
})
}
// Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced)
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",
Data: items,
}
}
// === HPP SUMMARY ===
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 })
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)
return SummaryHpp{
Label: label,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
}
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: "HPP dan Pengeluaran",
Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation),
},
ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation),
}
summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
}
}
// === PROFIT & LOSS ===
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
return PLItem{
Type: itemType,
FinancialMetrics: metrics,
}
}
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
return PLSummaryItem{
Label: label,
FinancialMetrics: metrics,
}
}
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 {
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"])
// 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)))
} 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
}
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)
// 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", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)),
}
}
func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem {
realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced)
return []PLItem{
ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)),
}
}
func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem {
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced)
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, totalOverheadPerBird := sumPLItems(overheadItems)
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
grossProfit := totalPenjualan - totalPembelian
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
totalOtherExpenses := totalOverhead + totalEkspedisi
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
netProfit := grossProfit - totalOtherExpenses
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)),
}
}
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,
}
}
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
return ProfitLossSection{
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
}
}
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,
ProfitLoss: profitLoss,
}
}
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
for _, chickin := range chickins {
totalPopulation += chickin.UsageQty
}
for _, delivery := range deliveryProducts {
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, actualPopulation, totalWeightProduced)
overheadItems := ToOverheadItems(budgets, realizations, actualPopulation, totalWeightProduced)
ekspedisiItems := ToEkspedisiItems(realizations, actualPopulation, totalWeightProduced)
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
}