Compare commits

..

3 Commits

33 changed files with 2099 additions and 2693 deletions
-1
View File
@@ -52,7 +52,6 @@ const (
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
)
const (
@@ -14,16 +14,14 @@ import (
)
type ClosingController struct {
ClosingService service.ClosingService
SapronakService service.SapronakService
ClosingKeuanganService service.ClosingKeuanganService
ClosingService service.ClosingService
SapronakService service.SapronakService
}
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
return &ClosingController{
ClosingService: closingService,
SapronakService: sapronakService,
ClosingKeuanganService: closingKeuanganService,
ClosingService: closingService,
SapronakService: sapronakService,
}
}
@@ -340,7 +338,7 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil {
return err
}
@@ -354,34 +352,6 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
})
}
func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan by kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
+4 -4
View File
@@ -111,13 +111,13 @@ type ClosingPerformanceDTO struct {
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"`
HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"`
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"`
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"`
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
}
type ClosingSalesGroupDTO struct {
@@ -1,103 +1,135 @@
package dto
// === CLOSING KEUANGAN CODES ===
import (
"slices"
"strings"
// Closing HPP Codes
type ClosingHPPCode string
const (
HPPCodePakan ClosingHPPCode = "PAKAN"
HPPCodeOVK ClosingHPPCode = "OVK"
HPPCodeDOC ClosingHPPCode = "DOC"
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// Closing Profit Loss Codes
type ClosingProfitLossCode string
// === CONSTANTS ===
const (
PLCodeSales ClosingProfitLossCode = "SALES"
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
HPPGroupPengeluaran = "HPP dan Pengeluaran"
HPPGroupBahanBaku = "HPP dan Bahan Baku"
HPPLabelOverhead = "Pengeluaran Overhead"
HPPLabelEkspedisi = "Beban Ekspedisi"
HPPSummaryLabel = "HPP"
PLSalesTypeChicken = "Penjualan Ayam Besar"
PLSalesTypeEgg = "Penjualan Telur"
PLItemTypeSapronak = "Pembelian Sapronak"
PLItemTypeOverhead = "Pengeluaran Overhead"
PLItemTypeEkspedisi = "Beban Ekspedisi"
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
PLSummaryLabelSubTotal = "SUB TOTAL"
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
PurchaseLabelPrefix = "Pembelian "
)
// === NEW CLOSING KEUANGAN DTO ===
// === CONTEXT STRUCTS ===
type CalculationContext struct {
TotalPopulation float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
TotalWeightSold float64
ActualPopulation float64
}
type ClosingKeuanganInput struct {
ProjectFlockCategory string
PurchaseItems []entities.PurchaseItem
Budgets []entities.ProjectBudget
Realizations []entities.ExpenseRealization
DeliveryProducts []entities.MarketingDeliveryProduct
Chickins []entities.ProjectChickin
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
}
// === BASE METRICS ===
// FinancialMetrics represents financial metrics with per unit and total amounts
type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
}
// HPPItem represents an item in HPP section
type HPPItem struct {
ID uint `json:"id"`
Category string `json:"category"` // "purchase" or "overhead"
Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI"
Label string `json:"label"`
type Comparison struct {
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
}
// HPPSummary represents summary for HPP section
type HPPSummary struct {
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
// === HPP PURCHASES PACKAGE ===
type HppItem struct {
Type string `json:"type"`
Comparison
}
// HPPSection represents HPP data section
type HPPSection struct {
Items []HPPItem `json:"items"`
Summary HPPSummary `json:"summary"`
type HppGroup struct {
GroupName string `json:"group_name"`
Data []HppItem `json:"data"`
}
// ProfitLossItem represents an item in Profit & Loss section
type ProfitLossItem struct {
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
Label string `json:"label"`
Type string `json:"type"` // "income", "purchase", "overhead"
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
type SummaryHpp struct {
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
}
// ProfitLossSummary represents summary for Profit & Loss section
type ProfitLossSummary struct {
GrossProfit FinancialMetrics `json:"gross_profit"`
SubTotal FinancialMetrics `json:"sub_total"`
NetProfit FinancialMetrics `json:"net_profit"`
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"`
}
// ProfitLossSection represents Profit & Loss data section
type ProfitLossSection struct {
Items []ProfitLossItem `json:"items"`
Summary ProfitLossSummary `json:"summary"`
Data ProfitLossData `json:"data"`
}
// ClosingKeuanganData represents the main data structure
type ClosingKeuanganData struct {
HPP HPPSection `json:"hpp"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === RESPONSE DTO (ROOT) ===
// ClosingKeuanganResponse represents the full API response
type ClosingKeuanganResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Data ClosingKeuanganData `json:"data"`
type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === MAPPER FUNCTIONS ===
// ToFinancialMetrics creates FinancialMetrics from values
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{
RpPerBird: rpPerBird,
@@ -106,80 +138,451 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
}
}
// ToHPPItem creates HPP item
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
return HPPItem{
ID: id,
Category: category,
Code: code,
Label: label,
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{
Budgeting: budgeting,
Realization: realization,
}
}
// ToHPPSummary creates HPP summary
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
return HPPSummary{
Label: label,
Budgeting: budgeting,
Realization: realization,
EggBudgeting: eggBudgeting,
EggRealization: eggRealization,
// === HPP PENGELUARAN (from Purchase Items) ===
func getFlagLabel(flagType utils.FlagType) string {
return PurchaseLabelPrefix + string(flagType)
}
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
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)
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
Comparison: ToComparison(
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
),
})
seenFlags[flagType] = true
}
}
}
return items
}
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelOverhead,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
),
}
}
// ToHPPSection creates HPP section
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
return HPPSection{
Items: items,
Summary: summary,
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
}
}
// ToProfitLossItem creates Profit & Loss item
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
return ProfitLossItem{
Code: code,
Label: label,
Type: itemType,
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
items := []HppItem{}
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
realizationAmount := getOperationalExpenses(realizations)
if budgetAmount > 0 || realizationAmount > 0 {
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
}
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
}
}
// ToProfitLossSummary creates Profit & Loss summary
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
return ProfitLossSummary{
GrossProfit: grossProfit,
SubTotal: subTotal,
NetProfit: netProfit,
// === HPP SUMMARY ===
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
purchaseTotal := sumPurchaseTotal(purchaseItems)
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
totalBudget := purchaseTotal + budgetTotal
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
summary.EggBudgeting = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: budgetEggRpPerKg,
Amount: totalBudget,
}
summary.EggRealization = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: realizationEggRpPerKg,
Amount: totalRealization,
}
}
return summary
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: HPPGroupPengeluaran,
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
},
ToHppBahanBakuGroup(budgets, realizations, ctx),
}
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
}
}
// ToProfitLossSection creates Profit & Loss section
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
// === 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 createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
for _, item := range items {
totalAmount += item.Amount
totalPerBird += item.RpPerBird
}
return
}
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
items := []PLItem{}
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
} else {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
}
return items
}
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
purchaseAmount := sumPurchaseTotal(purchases)
return []PLItem{
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
}
}
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
realizationAmount := getOperationalExpenses(realizations)
return []PLItem{
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
}
}
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
return []PLItem{
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
}
}
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(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
}
}
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
return ProfitLossData{
Penjualan: penjualanItems,
Pembelian: pembelianItems,
Overhead: totalOverhead,
Ekspedisi: totalEkspedisi,
Summary: summary,
}
}
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
return ProfitLossSection{
Items: items,
Summary: summary,
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
}
}
// ToClosingKeuanganData creates complete closing keuangan data
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
return ClosingKeuanganData{
HPP: hpp,
ProfitLoss: profitLoss,
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,
}
}
// ToSuccessClosingKeuanganResponse creates success response
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
return ClosingKeuanganResponse{
Code: 200,
Status: "success",
Message: "Get closing keuangan successfully",
Data: data,
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
var totalPopulation float64
var totalWeightSold float64
for _, chickin := range input.Chickins {
totalPopulation += chickin.UsageQty
}
for _, delivery := range input.DeliveryProducts {
totalWeightSold += delivery.TotalWeight
}
ctx := CalculationContext{
TotalPopulation: totalPopulation,
TotalWeightProduced: input.TotalWeightProduced,
TotalEggWeightKg: input.TotalEggWeightKg,
TotalDepletion: input.TotalDepletion,
TotalWeightSold: totalWeightSold,
ActualPopulation: totalPopulation - input.TotalDepletion,
}
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
overheadItems := ToOverheadItems(input.Realizations, ctx)
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
return ToReportResponse(hppSection, plSection)
}
// === 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 hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
for _, flag := range flags {
if strings.ToUpper(flag.Name) == string(flagType) {
return true
}
}
return false
}
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
return func(item *entities.PurchaseItem) bool {
if item.Product == nil || len(item.Product.Flags) == 0 {
return false
}
return hasProductFlag(item.Product.Flags, flagType)
}
}
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
}
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
}
}
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
hasFlag := filterRealizationByNonstockFlag(flagType)
return func(realization *entities.ExpenseRealization) bool {
return !hasFlag(realization)
}
}
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
amount := 0.0
for i := range items {
if filter(&items[i]) {
amount += extractor(&items[i])
}
}
return amount
}
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
}
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
}
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
}
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
}
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
}
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
}
func isChickenProductFlag(flagType utils.FlagType) bool {
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 PLSalesTypeChicken
}
for _, flag := range product.Flags {
flagType := utils.FlagType(strings.ToUpper(flag.Name))
if isEggProductFlag(flagType) {
return PLSalesTypeEgg
}
if isChickenProductFlag(flagType) {
return PLSalesTypeChicken
}
}
return PLSalesTypeChicken
}
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
}
@@ -0,0 +1,186 @@
package dto
// New Closing Keuangan Response DTO Structure
// Base metrics - digunakan di banyak tempat
type NewFinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
}
// Comparison untuk Budgeting vs Realization
type NewComparison struct {
Budgeting NewFinancialMetrics `json:"budgeting"`
Realization NewFinancialMetrics `json:"realization"`
}
// HPP Purchase Section
type HppPurchase struct {
Pakan NewComparison `json:"pakan"`
OVK NewComparison `json:"OVK"`
DOC NewComparison `json:"DOC"`
Depresiasi NewComparison `json:"Depresiasi"`
}
// HPP Overhead Section
type HppOverhead struct {
Overhead NewComparison `json:"overhead"`
Ekspedisi NewComparison `json:"ekspedisi"`
}
// Summary HPP
type NewSummaryHpp struct {
Label string `json:"label"`
Budgeting NewFinancialMetrics `json:"budgeting"`
Realization NewFinancialMetrics `json:"realization"`
EggBudgeting *NewFinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *NewFinancialMetrics `json:"egg_realization,omitempty"`
}
// HPP wrapper
type NewHpp struct {
HppPurchase HppPurchase `json:"hpp_purchase"`
HppOverhead HppOverhead `json:"hpp_overhead"`
SummaryHpp NewSummaryHpp `json:"summary_hpp"`
}
// Purchase Cost (dengan type field, embedded NewFinancialMetrics)
type PurchaseCost struct {
Type string `json:"type"`
NewFinancialMetrics
}
// PL Summary
type PLSummary struct {
GrossProfit NewFinancialMetrics `json:"gross_profit"`
SubTotal NewFinancialMetrics `json:"sub_total"`
NetProfit NewFinancialMetrics `json:"net_profit"`
}
// Profit Loss wrapper
type NewProfitLoss struct {
PenjualanTelur NewFinancialMetrics `json:"penjualan_telur"`
PurchaseCost PurchaseCost `json:"purchase_cost"`
Overhead NewFinancialMetrics `json:"overhead"`
Ekspedisi NewFinancialMetrics `json:"ekspedisi"`
Summary PLSummary `json:"summary"`
}
// Main Data structure
type NewClosingKeuanganData struct {
Hpp NewHpp `json:"hpp"`
ProfitLoss NewProfitLoss `json:"profit_loss"`
}
// Full Response DTO
type NewClosingKeuanganResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Data NewClosingKeuanganData `json:"data"`
}
// === MAPPER FUNCTIONS ===
// ToNewFinancialMetrics creates a new financial metrics
func ToNewFinancialMetrics(rpPerBird, rpPerKg, amount float64) NewFinancialMetrics {
return NewFinancialMetrics{
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
// ToNewComparison creates a new budgeting vs realization comparison
func ToNewComparison(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount, realizationRpPerBird, realizationRpPerKg, realizationAmount float64) NewComparison {
return NewComparison{
Budgeting: ToNewFinancialMetrics(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount),
Realization: ToNewFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
}
}
// ToHppPurchase creates HPP purchase section
func ToHppPurchase(pakan, oVK, dOC, depresiasi NewComparison) HppPurchase {
return HppPurchase{
Pakan: pakan,
OVK: oVK,
DOC: dOC,
Depresiasi: depresiasi,
}
}
// ToHppOverhead creates HPP overhead section
func ToHppOverhead(overhead, ekspedisi NewComparison) HppOverhead {
return HppOverhead{
Overhead: overhead,
Ekspedisi: ekspedisi,
}
}
// ToNewSummaryHpp creates HPP summary
func ToNewSummaryHpp(label string, budgeting, realization NewFinancialMetrics, eggBudgeting, eggRealization *NewFinancialMetrics) NewSummaryHpp {
return NewSummaryHpp{
Label: label,
Budgeting: budgeting,
Realization: realization,
EggBudgeting: eggBudgeting,
EggRealization: eggRealization,
}
}
// ToNewHpp creates complete HPP section
func ToNewHpp(hppPurchase HppPurchase, hppOverhead HppOverhead, summaryHpp NewSummaryHpp) NewHpp {
return NewHpp{
HppPurchase: hppPurchase,
HppOverhead: hppOverhead,
SummaryHpp: summaryHpp,
}
}
// ToPurchaseCost creates purchase cost item
func ToPurchaseCost(costType string, metrics NewFinancialMetrics) PurchaseCost {
return PurchaseCost{
Type: costType,
NewFinancialMetrics: metrics,
}
}
// ToPLSummary creates profit loss summary
func ToPLSummary(grossProfit, subTotal, netProfit NewFinancialMetrics) PLSummary {
return PLSummary{
GrossProfit: grossProfit,
SubTotal: subTotal,
NetProfit: netProfit,
}
}
// ToNewProfitLoss creates complete profit loss section
func ToNewProfitLoss(penjualanTelur, overhead, ekspedisi NewFinancialMetrics, purchaseCost PurchaseCost, summary PLSummary) NewProfitLoss {
return NewProfitLoss{
PenjualanTelur: penjualanTelur,
PurchaseCost: purchaseCost,
Overhead: overhead,
Ekspedisi: ekspedisi,
Summary: summary,
}
}
// ToNewClosingKeuanganData creates complete closing keuangan data
func ToNewClosingKeuanganData(hpp NewHpp, profitLoss NewProfitLoss) NewClosingKeuanganData {
return NewClosingKeuanganData{
Hpp: hpp,
ProfitLoss: profitLoss,
}
}
// ToSuccessNewClosingKeuanganResponse creates success response shortcut
func ToSuccessNewClosingKeuanganResponse(data NewClosingKeuanganData) NewClosingKeuanganResponse {
return NewClosingKeuanganResponse{
Code: 200,
Status: "success",
Message: "Get closing keuangan successfully",
Data: data,
}
}
+2 -3
View File
@@ -41,10 +41,9 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo)
closingService := sClosing.NewClosingService(closingRepo, closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
ClosingRoutes(router, userService, closingService, sapronakService)
}
@@ -10,7 +10,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
@@ -24,7 +23,7 @@ type ClosingRepository interface {
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
@@ -33,7 +32,6 @@ type ClosingRepository interface {
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
}
type ClosingRepositoryImpl struct {
@@ -63,11 +61,6 @@ type SapronakRow struct {
Notes string `gorm:"column:notes"`
}
type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"`
}
type SapronakQueryParams struct {
Type string
WarehouseIDs []uint
@@ -126,220 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil
}
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
var purchaseAgg struct {
TotalIn float64 `gorm:"column:total_in"`
}
err := r.DB().WithContext(ctx).
Table("purchase_items pi").
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
Where("f.name = ?", "PAKAN").
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
Scan(&purchaseAgg).Error
if err != nil {
return 0, 0, err
}
var usageAgg struct {
TotalUsed float64 `gorm:"column:total_used"`
}
err = r.DB().WithContext(ctx).
Table("recording_stocks rs").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", "PAKAN").
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
Scan(&usageAgg).Error
if err != nil {
return 0, 0, err
}
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var agg struct {
Total float64 `gorm:"column:total_culling"`
}
err := r.DB().WithContext(ctx).
Table("recording_depletions rd").
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagAyamCulling).
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.Total, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, nil
}
var agg struct {
TotalQty float64 `gorm:"column:total_qty"`
}
err := r.DB().WithContext(ctx).
Table("recording_eggs re").
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.TotalQty, nil
}
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
}
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
db := r.DB().WithContext(ctx)
if projectFlockID == 0 {
return nil, fmt.Errorf("invalid project flock id")
}
query := db.
Table("expense_realizations AS er").
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
Joins("JOIN expenses e ON e.id = ens.expense_id").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Joins("JOIN suppliers s ON s.id = e.supplier_id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("e.category = ?", "BOP").
Where("e.realization_date IS NOT NULL").
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
query = query.Where("pfk.id = ?", *projectFlockKandangID)
}
var rows []ExpeditionHPPRow
err := query.
Select(
"e.supplier_id AS supplier_id, " +
"s.name AS supplier_name, " +
"SUM(er.qty * er.price) AS total_amount",
).
Group("e.supplier_id, s.name").
Scan(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
const (
sapronakIncomingPurchasesSQL = `
SELECT
@@ -473,8 +252,8 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
COALESCE(fw.name, '') AS source_warehouse,
COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination,
'' AS destination_warehouse,
COALESCE(tw.name, '') AS destination,
std.usage_qty AS quantity,
u.name AS unit,
'Transfer to other unit' AS notes
@@ -522,8 +301,8 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
w.name AS source_warehouse,
'RETAIL CUSTOMER' AS destination_warehouse,
'' AS destination,
'' AS destination_warehouse,
'RETAIL CUSTOMER' AS destination,
mp.qty AS quantity,
u.name AS unit,
m.notes AS notes
@@ -889,58 +668,193 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
}
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
incomingQuery := r.withCtx(ctx).
Table("stock_transfer_details AS std").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
COALESCE(std.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll)
incoming, err := scanAndGroupDetails(incomingQuery)
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
if err != nil {
return nil, nil, err
}
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
return ref
}
return fmt.Sprintf("TRF-%d", row.ID)
})
return in, out, nil
}
outgoingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
outgoing, err := scanAndGroupDetails(outgoingQuery)
if err != nil {
return nil, nil, err
// === CLOSING DATA PRODUKSI METHODS ===
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
return incoming, outgoing, nil
var purchaseAgg struct {
TotalIn float64 `gorm:"column:total_in"`
}
err := r.DB().WithContext(ctx).
Table("purchase_items pi").
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
Where("f.name = ?", "PAKAN").
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
Scan(&purchaseAgg).Error
if err != nil {
return 0, 0, err
}
var usageAgg struct {
TotalUsed float64 `gorm:"column:total_used"`
}
err = r.DB().WithContext(ctx).
Table("recording_stocks rs").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", "PAKAN").
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
Scan(&usageAgg).Error
if err != nil {
return 0, 0, err
}
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var agg struct {
Total float64 `gorm:"column:total_culling"`
}
err := r.DB().WithContext(ctx).
Table("recording_depletions rd").
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagAyamCulling).
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.Total, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, nil
}
var agg struct {
TotalQty float64 `gorm:"column:total_qty"`
}
err := r.DB().WithContext(ctx).
Table("recording_eggs re").
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.TotalQty, nil
}
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
}
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -9,8 +9,8 @@ import (
"github.com/gofiber/fiber/v2"
)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
ctrl := controller.NewClosingController(s, sapronakSvc)
route := v1.Group("/closings")
route.Use(m.Auth(u))
@@ -34,6 +34,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
}
@@ -40,6 +40,7 @@ type ClosingService interface {
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
}
@@ -47,6 +48,7 @@ type closingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ClosingRepository
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository
@@ -61,11 +63,12 @@ type closingService struct {
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo,
@@ -336,10 +339,8 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
@@ -419,11 +420,14 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil
}
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
var ids []uint
query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID)
if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil {
return nil, err
@@ -575,12 +579,107 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
return &result, nil
}
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
s.Log.Infof("🔵 [CLOSING KEUANGAN] Starting fetch for ProjectFlockID: %d", projectFlockID)
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
s.Log.Infof("✅ [CLOSING KEUANGAN] ProjectFlock fetched: ID=%d, Category=%s, FlockName=%s",
projectFlock.Id, projectFlock.Category, projectFlock.FlockName)
// Validasi: Closing Keuangan hanya untuk LAYING, bukan GROWING
if projectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
s.Log.Warnf("⚠️ [CLOSING KEUANGAN] ProjectFlock ID %d is GROWING category, closing keuangan not available", projectFlockID)
return nil, fiber.NewError(fiber.StatusNotFound, "Closing keuangan only available for LAYING category")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
s.Log.Infof("💰 [CLOSING KEUANGAN] Budgets fetched: %d records", len(budgets))
actualUsageRows, err := s.ClosingKeuanganRepo.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
}
s.Log.Infof("📊 [CLOSING KEUANGAN] Actual Usage Costs fetched: %d records", len(actualUsageRows))
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
s.Log.Infof("🛒 [CLOSING KEUANGAN] Converted to Purchase Items: %d items", len(purchaseItems))
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
s.Log.Infof("💸 [CLOSING KEUANGAN] Expense Realizations fetched: %d records", len(realizations))
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
s.Log.Infof("🚚 [CLOSING KEUANGAN] Marketing Delivery Products fetched: %d records", len(deliveryProducts))
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
s.Log.Infof("🐣 [CLOSING KEUANGAN] Chickins fetched: %d records", len(chickins))
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
s.Log.Infof("⚖️ [CLOSING KEUANGAN] Total Weight Produced: %.2f kg", totalWeightProduced)
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
}
s.Log.Infof("🥚 [CLOSING KEUANGAN] Total Egg Weight: %.2f kg", totalEggWeightKg)
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion)
input := dto.ClosingKeuanganInput{
ProjectFlockCategory: projectFlock.Category,
PurchaseItems: purchaseItems,
Budgets: budgets,
Realizations: realizations,
DeliveryProducts: deliveryProducts,
Chickins: chickins,
TotalWeightProduced: totalWeightProduced,
TotalEggWeightKg: totalEggWeightKg,
TotalDepletion: totalDepletion,
}
report := dto.ToClosingKeuanganReport(input)
s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID)
return &report, nil
}
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
rows, err := s.ClosingKeuanganRepo.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
@@ -612,16 +711,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
var projectFlockKandangIDs []uint
if kandangID != nil && *kandangID > 0 {
projectFlockKandangIDs = []uint{*kandangID}
} else {
var err error
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
if len(projectFlockKandangIDs) == 0 {
@@ -882,16 +975,16 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = *productionStandardDetail.TargetEggMass
performance.EggMassStd = productionStandardDetail.TargetEggMass
}
}
}
@@ -1031,3 +1124,52 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
return closest.Mortality, closest.FcrNumber
}
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
if len(actualUsageRows) == 0 {
return []entity.PurchaseItem{}
}
// Collect all product IDs
productIDs := make([]uint, len(actualUsageRows))
for i, row := range actualUsageRows {
productIDs[i] = row.ProductID
}
// Fetch products with flags from repository
products, err := s.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs)
if err != nil {
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
products = []entity.Product{}
}
// Create product map
productMap := make(map[uint]*entity.Product)
for i := range products {
productMap[products[i].Id] = &products[i]
}
// Convert to pseudo purchase items
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
for _, row := range actualUsageRows {
product := productMap[row.ProductID]
// Skip if product not found
if product == nil {
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
continue
}
purchaseItem := entity.PurchaseItem{
Id: 0, // Pseudo item, no ID
ProductId: row.ProductID,
TotalQty: row.TotalQty,
TotalPrice: row.TotalPrice,
Price: row.AveragePrice,
Product: product,
}
purchaseItems = append(purchaseItems, purchaseItem)
}
return purchaseItems
}
@@ -1,640 +0,0 @@
package service
import (
"errors"
"strings"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// ClosingKeuanganService handles closing keuangan business logic
type ClosingKeuanganService interface {
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
}
type closingKeuanganService struct {
Log *logrus.Logger
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
RecordingRepo recordingRepository.RecordingRepository
}
func NewClosingKeuanganService(
closingKeuanganRepo repository.ClosingKeuanganRepository,
projectFlockRepo projectflockRepository.ProjectflockRepository,
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
chickinRepo chickinRepository.ProjectChickinRepository,
recordingRepo recordingRepository.RecordingRepository,
) ClosingKeuanganService {
return &closingKeuanganService{
Log: utils.Log,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
}
}
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
// Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
// Get all kandang for this project flock
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
}
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
// Validate and fetch project flock kandang
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
if kandang.ProjectFlockId != projectFlockID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
}
// Preload Nonstock.Flags manually
var budgetIDs []uint
for _, b := range budgets {
budgetIDs = append(budgetIDs, b.Id)
}
if len(budgetIDs) > 0 {
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
Preload("Nonstock.Flags").
Where("id IN ?", budgetIDs).
Find(&budgets).Error
}
kandangs := []entity.ProjectFlockKandang{*kandang}
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
}
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
// Define flag filters using constants
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
allFilters := append(pakanFilters, ovkFilters...)
allFilters = append(allFilters, ayamFilters...)
var allProductUsageRows []repository.ProductUsageRow
// Get ALL product usage
for _, kandang := range kandangs {
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
if err == nil {
allProductUsageRows = append(allProductUsageRows, rows...)
}
}
// Classify into categories based on flag priority
var pakanProductUsageRows []repository.ProductUsageRow
var ovkProductUsageRows []repository.ProductUsageRow
var ayamProductUsageRows []repository.ProductUsageRow
for _, row := range allProductUsageRows {
// Parse flag names from comma-separated string
flagNames := strings.Split(row.FlagNames, ",")
hasPakanFlag := false
hasOvkFlag := false
hasAyamFlag := false
for _, flag := range flagNames {
flag = strings.TrimSpace(flag)
if containsItem(pakanFilters, flag) {
hasPakanFlag = true
}
if containsItem(ovkFilters, flag) {
hasOvkFlag = true
}
if containsItem(ayamFilters, flag) {
hasAyamFlag = true
}
}
// Priority: PAKAN > OVK > AYAM
if hasPakanFlag {
pakanProductUsageRows = append(pakanProductUsageRows, row)
} else if hasOvkFlag {
ovkProductUsageRows = append(ovkProductUsageRows, row)
} else if hasAyamFlag {
ayamProductUsageRows = append(ayamProductUsageRows, row)
} else {
continue
}
}
// Calculate total price for each category
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
for _, row := range pakanProductUsageRows {
totalPakanPrice += row.TotalPengeluaran
}
for _, row := range ovkProductUsageRows {
totalOvkPrice += row.TotalPengeluaran
}
for _, row := range ayamProductUsageRows {
totalAyamPrice += row.TotalPengeluaran
}
// Determine if this is per-kandang or per-project-flock scope
isPerKandang := len(kandangs) == 1
var projectFlockKandangID *uint
if isPerKandang {
kandangID := kandangs[0].Id
projectFlockKandangID = &kandangID
}
var err error
// Fetch realizations
var realizations []entity.ExpenseRealization
if isPerKandang && projectFlockKandangID != nil {
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
} else {
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
}
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
db = db.Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product")
return db
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
}
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
if isPerKandang && projectFlockKandangID != nil {
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
for _, dp := range deliveryProducts {
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
filteredProducts = append(filteredProducts, dp)
}
}
deliveryProducts = filteredProducts
}
// Fetch chickins
var chickins []entity.ProjectChickin
if isPerKandang && projectFlockKandangID != nil {
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
}
// Get total depletion
var totalDepletion float64
if isPerKandang && projectFlockKandangID != nil {
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
totalDepletion = 0
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
if err != nil {
}
// Try to get actual weight from uniformity data
var totalWeightFromUniformity float64
if isPerKandang && projectFlockKandangID != nil {
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
} else if totalWeightFromUniformity > 0 {
totalWeightProduced = totalWeightFromUniformity
}
// Fetch egg data only for Laying category
var totalEggWeightKg float64
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// TODO: Replace with actual method to get egg weight from RecordingRepo
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
// For now, set to 0 as placeholder
totalEggWeightKg = 0
} else {
totalEggWeightKg = 0
}
// Build new DTO structure
// Calculate totals
var totalPopulation float64
for _, chickin := range chickins {
totalPopulation += chickin.UsageQty
}
// Calculate actual population (total population - depletion)
actualPopulation := totalPopulation - totalDepletion
// Calculate budget totals by category
calculateBudgetByFlag := func(flags []string) float64 {
var total float64
for _, budget := range budgets {
if budget.Nonstock != nil {
for _, nonstockFlag := range budget.Nonstock.Flags {
flagName := strings.ToUpper(nonstockFlag.Name)
for _, targetFlag := range flags {
if flagName == strings.ToUpper(targetFlag) {
total += budget.Price * budget.Qty
break
}
}
}
}
}
return total
}
// Budget per category
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
totalBudgetAmount := 0.0
for _, budget := range budgets {
totalBudgetAmount += budget.Price * budget.Qty
}
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
// Calculate realization totals
var totalRealizationAmount float64
var totalEkspedisiRealization float64
for _, realization := range realizations {
amount := realization.Price * realization.Qty
totalRealizationAmount += amount
// Check if this is ekspedisi (need to check nonstock flags)
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
if flag.Name == "EKSPEDISI" {
totalEkspedisiRealization += amount
break
}
}
}
}
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
// Filter delivery products based on category
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
for _, delivery := range deliveryProducts {
// Get product from delivery
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
product := delivery.MarketingProduct.ProductWarehouse.Product
isEggProduct := false
isChickenProduct := false
// Check product flags
for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
// Egg product flags
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
isEggProduct = true
}
// Chicken product flags
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
isChickenProduct = true
}
}
// Filter based on project flock category
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
// Laying: only egg products
if isEggProduct {
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
} else {
// Growing/Contract Growing: only chicken products
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
// Include if chicken product or if no specific flags (default to chicken)
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
}
}
}
// Calculate total weight sold and sales amount from filtered products
var totalWeightSold float64
var totalSalesAmount float64
for _, delivery := range filteredDeliveryProducts {
totalWeightSold += delivery.TotalWeight
totalSalesAmount += delivery.TotalPrice
}
// Calculate metrics - always use kg ayam for rp_per_kg
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation // Use actual population
}
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
return
}
// Calculate metrics for profit loss (use total population and total weight produced)
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if totalPopulation > 0 {
rpPerBird = amount / totalPopulation
}
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
return
}
// Build HPP Items using constants
hppItems := []dto.HPPItem{}
// PAKAN item
pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
hppItems = append(hppItems, dto.ToHPPItem(
1,
"purchase",
string(dto.HPPCodePakan),
"Pembelian Pakan",
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
))
// OVK item
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
hppItems = append(hppItems, dto.ToHPPItem(
2,
"purchase",
string(dto.HPPCodeOVK),
"Pembelian OVK",
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
))
// DOC/DEPRESIASI item
docCode := string(dto.HPPCodeDOC)
docLabel := "Pembelian DOC"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
docCode = string(dto.HPPCodeDepresiasi)
docLabel = "Depresiasi"
}
docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
hppItems = append(hppItems, dto.ToHPPItem(
3,
"purchase",
docCode,
docLabel,
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
))
// OVERHEAD item
overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
hppItems = append(hppItems, dto.ToHPPItem(
4,
"overhead",
string(dto.HPPCodeOverhead),
"Pengeluaran Overhead",
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
))
// EKSPEDISI item
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
hppItems = append(hppItems, dto.ToHPPItem(
5,
"overhead",
string(dto.HPPCodeEkspedisi),
"Beban Ekspedisi",
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
))
// HPP Summary
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
var eggBudgeting, eggRealization *dto.FinancialMetrics
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
eggBudgeting = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: eggBudgetRpPerKg,
Amount: totalBudgetHpp,
}
eggRealization = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: eggRealizationRpPerKg,
Amount: totalRealizationHpp,
}
}
hppSummary := dto.ToHPPSummary(
"HPP",
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
eggBudgeting,
eggRealization,
)
hppSection := dto.ToHPPSection(hppItems, hppSummary)
// Build Profit Loss Items using constants
plItems := []dto.ProfitLossItem{}
// SALES item
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
salesLabel = "Penjualan Telur"
}
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSales),
salesLabel,
"income",
salesRpPerBird,
salesRpPerKg,
totalSalesAmount,
))
// SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
sapronakLabel := "Pengeluaran Sapronak"
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak),
sapronakLabel,
"purchase",
sapronakRpPerBird,
sapronakRpPerKg,
totalSapronakAmount,
))
// OVERHEAD item
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead),
"Overhead",
"overhead",
overheadRpPerBird,
overheadRpPerKg,
totalOperationalRealization,
))
// EKSPEDISI item
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi),
"Ekspedisi",
"overhead",
ekspedisiRealizationRpPerBird,
ekspedisiRealizationRpPerKg,
totalEkspedisiRealization,
))
// Profit Loss Summary
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
// Gross Profit should NOT include overhead and ekspedisi
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
costOfGoodsSoldRpPerBird := sapronakRpPerBird
grossProfit := totalSalesAmount - costOfGoodsSold
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
// Operating Expenses (Overhead + Ekspedisi)
totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
// Net Profit = Gross Profit - Operating Expenses
netProfit := grossProfit - totalOperatingExpenses
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
plSummary := dto.ToProfitLossSummary(
dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
)
profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
// Build complete response
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
return &data, nil
}
// containsItem checks if a string exists in a slice
func containsItem(slice []string, item string) bool {
for _, s := range slice {
if strings.EqualFold(s, item) {
return true
}
}
return false
}
@@ -3,6 +3,7 @@ package service
import (
"context"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -111,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
}
// We no longer filter by date for closing sapronak report; pass nil pointers.
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
if err != nil {
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
@@ -125,6 +126,8 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
KandangName: pfk.Kandang.Name,
Period: pfk.Period,
Status: status,
StartDate: nil,
EndDate: nil,
TotalIncomingValue: totalIncoming,
TotalUsageValue: totalUsage,
Items: items,
@@ -315,7 +318,7 @@ func buildSapronakDetails(
return result
}
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
// For sapronak closing report we intentionally ignore date range
// and aggregate all historical transactions for the kandang/project.
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
@@ -416,22 +419,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return groupMap[flag]
}
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if flag == "" && len(details) > 0 {
flag = details[0].Flag
}
if name == "" && len(details) > 0 {
name = details[0].ProductName
}
return flag, name
}
for _, row := range incoming {
if !matchesFlag(row.Flag) {
continue
@@ -567,18 +554,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range incomingDetails {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -587,18 +575,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range adjIncoming {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -607,18 +596,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range usageDetails {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -626,18 +616,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range adjOutgoing {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -645,18 +636,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range transIncoming {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai
@@ -665,18 +657,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
for productID, details := range transOutgoing {
flag, name := resolveFlagName(productID, details)
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
@@ -249,7 +249,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * totalWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -363,7 +363,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * totalWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -294,7 +294,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * totalWeight
totalPrice := rp.UnitPrice * rp.Qty
updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId,
@@ -594,7 +594,7 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * totalWeight
totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId,
@@ -14,7 +14,6 @@ type CustomerRelationDTO struct {
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
Address string `json:"address,omitempty"`
Balance float64 `json:"balance"`
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
}
@@ -53,8 +52,6 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
Name: e.Name,
Type: e.Type,
AccountNumber: e.AccountNumber,
Address: e.Address,
Balance: e.Balance,
Pic: pic,
}
}
@@ -28,7 +28,6 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
Category: c.Query("category", ""),
Flag: c.Query("flag", ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -47,10 +47,6 @@ func (s supplierService) withRelations(db *gorm.DB) *gorm.DB {
Preload("NonstockSuppliers.Nonstock.Flags")
}
func (s supplierService) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -67,7 +63,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
offset := (params.Page - 1) * params.Limit
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withListRelations(db)
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
}
@@ -76,23 +72,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
}
if params.Flag != "" {
flag := strings.ToUpper(params.Flag)
db = db.Where(`
EXISTS (
SELECT 1
FROM nonstock_suppliers nsup
JOIN nonstocks n ON n.id = nsup.nonstock_id
JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?
WHERE nsup.supplier_id = suppliers.id
AND UPPER(f.name) = ?
)`,
entity.FlagableTypeNonstock,
flag,
)
}
return db.Order("suppliers.created_at DESC").Order("suppliers.updated_at DESC")
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
@@ -32,8 +32,7 @@ type Update struct {
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1"`
Flag string `query:"flag" validate:"omitempty"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
Category string `query:"category" validate:"omitempty,max=50"`
}
@@ -110,11 +110,7 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
}
typ := strings.ToUpper(req.Type)
createValidationOpts := WarehouseTypeValidationOptions{
LocationProvided: req.LocationId != nil,
KandangProvided: req.KandangId != nil,
}
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil {
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
return nil, err
}
@@ -212,22 +208,9 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
finalKandangId = req.KandangId
}
originalLocationId := finalLocationId
originalKandangId := finalKandangId
updateValidationOpts := WarehouseTypeValidationOptions{
AutoClear: true,
LocationProvided: req.LocationId != nil,
KandangProvided: req.KandangId != nil,
}
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, &finalLocationId, &finalKandangId, updateValidationOpts); err != nil {
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
return nil, err
}
if originalLocationId != finalLocationId {
updateBody["location_id"] = nil
}
if originalKandangId != finalKandangId {
updateBody["kandang_id"] = nil
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
@@ -255,65 +238,47 @@ func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil
}
type WarehouseTypeValidationOptions struct {
AutoClear bool
LocationProvided bool
KandangProvided bool
}
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID **uint, kandangID **uint, opts WarehouseTypeValidationOptions) error {
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
switch utils.WarehouseType(typ) {
case utils.WarehouseTypeArea:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
}
if locationID != nil && *locationID != nil {
if opts.AutoClear && !opts.LocationProvided {
*locationID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if locationID != nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if kandangID != nil && *kandangID != nil {
if opts.AutoClear && !opts.KandangProvided {
*kandangID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
return nil
case utils.WarehouseTypeLokasi:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
}
if locationID == nil || *locationID == nil {
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
}
if **locationID == 0 {
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
}
if kandangID != nil && *kandangID != nil {
if opts.AutoClear && !opts.KandangProvided {
*kandangID = nil
} else {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
return nil
case utils.WarehouseTypeKandang:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
}
if locationID == nil || *locationID == nil {
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
}
if **locationID == 0 {
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
}
if kandangID == nil || *kandangID == nil {
if kandangID == nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
}
if **kandangID == 0 {
if *kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
}
return nil
@@ -584,6 +584,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil
}
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
if s.ProductRepo == nil {
return nil
@@ -1,7 +1,6 @@
package dto
import (
"strconv"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -54,7 +53,6 @@ type ProjectFlockKandangListDTO struct {
ProjectFlockKandangRelationDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
NameWithPeriod string `json:"name_with_period"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
@@ -106,7 +104,6 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -129,16 +126,6 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
return &mapped
}
func toNameWithPeriod(kandang entity.Kandang, period int) string {
if kandang.Name == "" {
return ""
}
if period == 0 {
return kandang.Name
}
return kandang.Name + " Period " + strconv.Itoa(period)
}
func toApprovalDTOSelector(
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
approval := selector(e)
@@ -160,7 +147,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -44,7 +44,6 @@ type RecordingRepository interface {
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, 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)
@@ -549,30 +548,3 @@ func nextRecordingDay(days []int) int {
return len(normalized) + 1
}
// GetTotalWeightProducedFromUniformityByProjectFlockID calculates total weight produced from uniformity data
// It takes the latest uniformity record per kandang and calculates: SUM(mean_weight * chick_qty_of_weight / 1000)
func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
if projectFlockID == 0 {
return 0, nil
}
var result struct {
TotalWeight float64
}
err := r.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity").
Select("COALESCE(SUM((mean_up / 1.10) * chick_qty_of_weight / 1000), 0) as total_weight").
Joins("JOIN ("+
" SELECT pfku.project_flock_kandang_id, MAX(pfku.id) as latest_id "+
" FROM project_flock_kandang_uniformity pfku "+
" JOIN project_flock_kandangs pfk ON pfk.id = pfku.project_flock_kandang_id "+
" WHERE pfk.project_flock_id = ? "+
" GROUP BY pfku.project_flock_kandang_id "+
") latest ON latest.project_flock_kandang_id = project_flock_kandang_uniformity.project_flock_kandang_id "+
"AND project_flock_kandang_uniformity.id = latest.latest_id", projectFlockID).
Scan(&result).Error
return result.TotalWeight, err
}
@@ -36,7 +36,6 @@ type ExpenseReceivingPayload struct {
TransportPerItem *float64
ReceivedQty float64
ReceivedDate *time.Time
VehicleNumber *string
}
type groupedItem struct {
@@ -183,22 +182,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}
ctx := c.Context()
filtered := make([]ExpenseReceivingPayload, 0, len(updates))
for _, upd := range updates {
if upd.SupplierID == 0 {
continue
}
if upd.TransportPerItem == nil || *upd.TransportPerItem <= 0 {
continue
}
if upd.VehicleNumber == nil || strings.TrimSpace(*upd.VehicleNumber) == "" {
continue
}
filtered = append(filtered, upd)
}
if len(filtered) == 0 {
return nil
}
// Load current links to decide whether to update in place or recreate.
type itemLink struct {
@@ -222,9 +205,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
itemLinks := make(map[uint]itemLink)
updatedExpenses := make(map[uint64]struct{})
if len(filtered) > 0 {
ids := make([]uint, 0, len(filtered))
for _, upd := range filtered {
if len(updates) > 0 {
ids := make([]uint, 0, len(updates))
for _, upd := range updates {
if upd.PurchaseItemID != 0 {
ids = append(ids, upd.PurchaseItemID)
}
@@ -269,7 +252,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
groups := make(map[string][]groupedItem)
for _, payload := range filtered {
for _, payload := range updates {
if payload.ReceivedDate == nil {
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
}
@@ -702,7 +702,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
warehouseID uint
supplierID uint
transportPerItem *float64
vehicleNumber *string
overrideWarehouse bool
receivedQty float64
}
@@ -757,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
}
visitedItems[payload.PurchaseItemID] = struct{}{}
var supplierID uint
supplierID := purchase.SupplierId
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
supplierID = *payload.ExpeditionVendorID
}
@@ -771,15 +770,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
transportPerItem = &val
}
var vehicleNumber *string
if payload.VehicleNumber != nil && strings.TrimSpace(*payload.VehicleNumber) != "" {
val := strings.TrimSpace(*payload.VehicleNumber)
vehicleNumber = &val
} else if item.VehicleNumber != nil && strings.TrimSpace(*item.VehicleNumber) != "" {
val := strings.TrimSpace(*item.VehicleNumber)
vehicleNumber = &val
}
prepared = append(prepared, preparedReceiving{
item: item,
payload: payload,
@@ -787,7 +777,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
warehouseID: warehouseID,
supplierID: supplierID,
transportPerItem: transportPerItem,
vehicleNumber: vehicleNumber,
overrideWarehouse: overrideWarehouse,
receivedQty: receivedQty,
})
@@ -975,7 +964,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
TransportPerItem: prep.transportPerItem,
ReceivedQty: prep.receivedQty,
ReceivedDate: &date,
VehicleNumber: prep.vehicleNumber,
}
receivingPayloads = append(receivingPayloads, payload)
}
@@ -242,65 +242,6 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
var customerIDs []uint
if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" {
ids := strings.Split(customerIDsStr, ",")
for _, idStr := range ids {
idStr = strings.TrimSpace(idStr)
if idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
customerIDs = append(customerIDs, uint(id))
}
}
}
}
query := &validation.CustomerPaymentQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
CustomerIDs: customerIDs,
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
}
// Validate pagination
if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided")
}
result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query)
if err != nil {
return err
}
// If single customer mode (only 1 customer ID), return without pagination
if len(customerIDs) == 1 {
return ctx.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get customer payment report successfully",
Data: result,
})
}
// Multiple customers mode with pagination
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get customer payment report successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: result,
})
}
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
idParam := ctx.Params("idProjectFlockKandang")
if idParam == "" {
@@ -1,120 +0,0 @@
package dto
import (
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
)
type CustomerPaymentReportRow struct {
TransactionType string `json:"transaction_type"`
TransactionID int64 `json:"transaction_id"`
TransDate time.Time `json:"trans_date"`
DeliveryDate *time.Time `json:"delivery_date"`
Reference string `json:"reference"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AverageWeight float64 `json:"average_weight"`
UnitPrice float64 `json:"unit_price"`
FinalPrice float64 `json:"final_price"`
TotalPrice float64 `json:"total_price"`
PaymentAmount float64 `json:"payment_amount"`
AccountsReceivable float64 `json:"accounts_receivable"`
AgingDay *int `json:"aging_day"`
Status string `json:"status"`
VehicleNumbers []string `json:"vehicle_numbers"`
PickupInfo []string `json:"pickup_info"`
SalesPerson string `json:"sales_person"`
}
type CustomerPaymentReportSummary struct {
TotalQty float64 `json:"total_qty"`
TotalWeight float64 `json:"total_weight"`
TotalFinalAmount float64 `json:"total_final_amount"`
TotalGrandAmount float64 `json:"total_grand_amount"`
TotalPayment float64 `json:"total_payment"`
TotalAccountsReceivable float64 `json:"total_accounts_receivable"`
}
type CustomerPaymentReportItem struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
InitialBalance float64 `json:"initial_balance"`
Rows []CustomerPaymentReportRow `json:"rows"`
Summary CustomerPaymentReportSummary `json:"summary"`
}
type CustomerPaymentReportResponse struct {
Data []CustomerPaymentReportItem `json:"data"`
}
func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) CustomerPaymentReportRow {
return CustomerPaymentReportRow{
TransactionType: tx.TransactionType,
TransactionID: tx.TransactionID,
TransDate: tx.TransDate,
DeliveryDate: tx.DeliveryDate,
Reference: tx.Reference,
Qty: tx.Qty,
Weight: tx.Weight,
AverageWeight: tx.AverageWeight,
UnitPrice: tx.Price,
FinalPrice: tx.FinalPrice,
TotalPrice: tx.TotalPrice,
PaymentAmount: tx.PaymentAmount,
VehicleNumbers: parseStringSlice(tx.VehicleNumbers),
PickupInfo: parseStringSlice(tx.PickupInfo),
SalesPerson: tx.SalesPerson,
}
}
func ToCustomerPaymentReportItem(customer entities.Customer, initialBalance float64, rows []CustomerPaymentReportRow, summary CustomerPaymentReportSummary) CustomerPaymentReportItem {
return CustomerPaymentReportItem{
Customer: customerDTO.ToCustomerRelationDTO(customer),
InitialBalance: initialBalance,
Rows: rows,
Summary: summary,
}
}
func ToCustomerPaymentReportSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary {
summary := CustomerPaymentReportSummary{}
for _, row := range rows {
summary.TotalQty += row.Qty
summary.TotalWeight += row.Weight
if row.TransactionType == "SALES" {
summary.TotalFinalAmount += row.FinalPrice
summary.TotalGrandAmount += row.TotalPrice
} else if row.TransactionType == "PAYMENT" {
summary.TotalPayment += row.PaymentAmount
}
}
// Total AR = Initial Balance - Total Sales + Total Payment
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary
}
func parseStringSlice(str string) []string {
str = strings.TrimSpace(str)
if str == "" || str == "-" {
return []string{}
}
parts := strings.Split(str, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
}
+35 -30
View File
@@ -25,15 +25,14 @@ type HppPerKandangResponseData struct {
}
type HppPerKandangRowDTO struct {
ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
NameWithPeriode string `json:"name_with_periode"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"`
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
// FeedCostRp float64 `json:"feed_cost_rp"`
// OvkCostRp float64 `json:"ovk_cost_rp"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
@@ -41,8 +40,8 @@ type HppPerKandangRowDTO struct {
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
// HppRp float64 `json:"hpp_rp"`
// RemainingValueRp int64 `json:"remaining_value_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
}
type HppPerKandangRowKandangDTO struct {
@@ -81,28 +80,34 @@ type HppPerKandangSummaryDTO struct {
}
type HppPerKandangSummaryWeightRangeDTO struct {
ID int `json:"id"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
Label string `json:"label"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
EggValueRp int64 `json:"egg_value_rp"`
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
ID int `json:"id"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
Label string `json:"label"`
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
EggValueRp int64 `json:"egg_value_rp"`
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
}
type HppPerKandangSummaryTotalDTO struct {
AverageWeightKg float64 `json:"average_weight_kg"`
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
TotalEggValueRp int64 `json:"total_egg_value_rp"`
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"`
TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"`
AverageWeightKg float64 `json:"average_weight_kg"`
TotalRemainingValueRp int64 `json:"total_remaining_value_rp"`
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
TotalEggValueRp int64 `json:"total_egg_value_rp"`
TotalHppRp float64 `json:"total_hpp_rp"`
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
}
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
+15 -4
View File
@@ -12,7 +12,6 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
productionStandardRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -36,14 +35,26 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
customerRepository := customerRepo.NewCustomerRepository(db)
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
repportService := sRepport.NewRepportService(
validate,
expenseRealizationRepository,
marketingDeliveryProductRepository,
purchaseRepository,
chickinRepository,
recordingRepository,
approvalSvc,
purchaseSupplierRepository,
debtSupplierRepository,
hppPerKandangRepository,
productionResultRepository,
standardGrowthDetailRepository,
productionStandardDetailRepository,
)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -1,195 +0,0 @@
package repositories
import (
"context"
"time"
"gorm.io/gorm"
)
type CustomerPaymentTransaction struct {
TransactionType string `gorm:"column:transaction_type"`
TransactionID int64 `gorm:"column:transaction_id"`
CustomerID int64 `gorm:"column:customer_id"`
TransDate time.Time `gorm:"column:trans_date"`
DeliveryDate *time.Time `gorm:"column:delivery_date"`
Reference string `gorm:"column:reference"`
VehicleNumbers string `gorm:"column:vehicle_numbers"`
Qty float64 `gorm:"column:qty"`
Weight float64 `gorm:"column:weight"`
AverageWeight float64 `gorm:"column:average_weight"`
Price float64 `gorm:"column:price"`
FinalPrice float64 `gorm:"column:final_price"`
TotalPrice float64 `gorm:"column:total_price"`
PaymentAmount float64 `gorm:"column:payment_amount"`
PickupInfo string `gorm:"column:pickup_info"`
SalesPerson string `gorm:"column:sales_person"`
}
type CustomerPaymentRepository interface {
GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error)
GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error)
GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error)
}
type customerPaymentRepositoryImpl struct {
db *gorm.DB
}
func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository {
return &customerPaymentRepositoryImpl{db: db}
}
func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) {
salesQuery := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`
'SALES' AS transaction_type,
mdp.id::BIGINT AS transaction_id,
c.id::BIGINT AS customer_id,
m.so_date::DATE AS trans_date,
mdp.delivery_date::DATE AS delivery_date,
m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference,
COALESCE(mdp.vehicle_number, '') AS vehicle_numbers,
COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty,
COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight,
COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight,
COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS total_price,
0::NUMERIC(15,3) AS payment_amount,
w.name AS pickup_info,
u.name AS sales_person
`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Joins("INNER JOIN customers c ON c.id = m.customer_id").
Joins("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("INNER JOIN users u ON u.id = m.sales_person_id").
Where("mdp.delivery_date IS NOT NULL").
Where("m.deleted_at IS NULL").
Where("c.deleted_at IS NULL")
if customerID != nil {
salesQuery = salesQuery.Where("c.id = ?", *customerID)
}
paymentQuery := r.db.WithContext(ctx).
Table("payments p").
Select(`
'PAYMENT' AS transaction_type,
p.id::BIGINT AS transaction_id,
c.id::BIGINT AS customer_id,
p.payment_date::DATE AS trans_date,
NULL AS delivery_date,
COALESCE(p.reference_number, p.payment_code) AS reference,
'-' AS vehicle_numbers,
0::NUMERIC(15,3) AS qty,
0::NUMERIC(15,3) AS weight,
0::NUMERIC(15,3) AS average_weight,
0::NUMERIC(15,3) AS price,
0::NUMERIC(15,3) AS final_price,
0::NUMERIC(15,3) AS total_price,
p.nominal::NUMERIC(15,3) AS payment_amount,
'-' AS pickup_info,
'-' AS sales_person
`).
Joins("INNER JOIN customers c ON c.id = p.party_id").
Where("p.party_type = ?", "CUSTOMER").
Where("p.direction = ?", "IN").
Where("p.transaction_type = ?", "PENJUALAN").
Where("p.deleted_at IS NULL").
Where("c.deleted_at IS NULL")
if customerID != nil {
paymentQuery = paymentQuery.Where("c.id = ?", *customerID)
}
var results []CustomerPaymentTransaction
err := r.db.WithContext(ctx).
Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id",
salesQuery,
paymentQuery,
).
Scan(&results).
Error
if err != nil {
return nil, err
}
return results, nil
}
func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) {
var result struct {
Nominal float64
}
err := r.db.WithContext(ctx).
Table("payments").
Select("COALESCE(SUM(nominal), 0) as nominal").
Where("party_type = ?", "CUSTOMER").
Where("party_id = ?", customerID).
Where("transaction_type = ?", "SALDO_AWAL").
Where("deleted_at IS NULL").
Scan(&result).
Error
if err != nil {
return 0, err
}
return result.Nominal, nil
}
func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) {
subQuery := r.db.WithContext(ctx).
Table("(" +
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " +
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " +
"INNER JOIN marketings m ON m.id = mp.marketing_id " +
"INNER JOIN customers c ON c.id = m.customer_id " +
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " +
"UNION " +
"SELECT DISTINCT c.id as customer_id FROM payments p " +
"INNER JOIN customers c ON c.id = p.party_id " +
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " +
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" +
") as customer_ids")
var total int64
if err := subQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
var customerIDs []uint
err := r.db.WithContext(ctx).
Table("("+
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+
"INNER JOIN marketings m ON m.id = mp.marketing_id "+
"INNER JOIN customers c ON c.id = m.customer_id "+
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+
"UNION "+
"SELECT DISTINCT c.id as customer_id FROM payments p "+
"INNER JOIN customers c ON c.id = p.party_id "+
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+
") as customer_ids").
Select("customer_id").
Order("customer_id ASC").
Limit(limit).
Offset(offset).
Pluck("customer_id", &customerIDs).
Error
if err != nil {
return nil, 0, err
}
return customerIDs, total, nil
}
@@ -11,46 +11,40 @@ import (
)
type HppPerKandangRow struct {
ProjectFlockKandangID uint
ProjectFlockPeriod int
KandangID uint
KandangName string
KandangStatus string
LocationID uint
LocationName string
PicID uint
PicName string
RecordingCount int64
// RemainingChickenBirds float64
// RemainingChickenWeight float64
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
KandangID uint
KandangName string
KandangStatus string
LocationID uint
LocationName string
PicID uint
PicName string
RemainingChickenBirds float64
RemainingChickenWeight float64
EggProductionWeightKg float64
EggProductionPieces float64
}
type HppPerKandangCostRow struct {
ProjectFlockKandangID uint
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
KandangID uint
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
}
type HppPerKandangSupplierRow struct {
ProjectFlockKandangID uint
SupplierID uint
SupplierName string
SupplierAlias string
Category string
KandangID uint
SupplierID uint
SupplierName string
SupplierAlias string
Category string
}
type HppPerKandangRepository interface {
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
}
type hppPerKandangRepository struct {
@@ -64,32 +58,9 @@ func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository {
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
var rows []HppPerKandangRow
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
validRecordings := r.db.WithContext(ctx).
Table("recordings AS r").
Select("r.id, r.project_flock_kandangs_id, r.total_chick_qty").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
query := r.db.WithContext(ctx).
Table("project_flocks AS pf").
Table("recordings AS r").
Select(`
pfk.id AS project_flock_kandang_id,
pfk.period AS project_flock_period,
k.id AS kandang_id,
k.name AS kandang_name,
k.status AS kandang_status,
@@ -97,31 +68,23 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
loc.name AS location_name,
pic.id AS pic_id,
pic.name AS pic_name,
COALESCE(COUNT(vr.id), 0) AS recording_count,
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
0 AS remaining_chicken_weight,
0 AS egg_production_weight_kg,
0 AS egg_production_pieces,
0 AS egg_production_total_weight_kg,
0 AS egg_production_total_pieces`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins(`
LEFT JOIN (
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
FROM project_chickins
GROUP BY project_flock_kandang_id
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds,
COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight,
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN users AS pic ON pic.id = k.pic_id").
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start)
Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id").
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("pfk.id ASC")
query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
@@ -130,44 +93,41 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
return rows, nil
}
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
var rows []HppPerKandangCostRow
recordingPfk := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT pfk.id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
pfk.id AS project_flock_kandang_id,
k.id AS kandang_id,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS feed_cost,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS ovk_cost`,
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
@@ -176,30 +136,31 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
query = query.Group("pfk.id").Order("pfk.id ASC")
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("k.id").Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, nil, err
}
docRows := make([]struct {
ProjectFlockKandangID uint
DocCost float64
DocQty float64
SupplierID *uint
SupplierName *string
SupplierAlias *string
KandangID uint
DocCost float64
DocQty float64
SupplierID *uint
SupplierName *string
SupplierAlias *string
}, 0)
docQuery := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
pfk.id AS project_flock_kandang_id,
pfk.kandang_id AS kandang_id,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
s.id AS supplier_id,
@@ -211,8 +172,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Group("pfk.id, s.id, s.name, s.alias")
Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("pfk.kandang_id, s.id, s.name, s.alias")
docQuery = applyLocationFilters(docQuery, areaIDs, locationIDs, kandangIDs)
if err := docQuery.Scan(&docRows).Error; err != nil {
return nil, nil, err
@@ -221,28 +183,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
for i := range rows {
row := rows[i]
costMap[row.ProjectFlockKandangID] = &rows[i]
costMap[row.KandangID] = &rows[i]
}
docSuppliers := make([]HppPerKandangSupplierRow, 0)
docSeen := make(map[uint]map[uint]bool)
for _, doc := range docRows {
entry, ok := costMap[doc.ProjectFlockKandangID]
entry, ok := costMap[doc.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: doc.ProjectFlockKandangID,
KandangID: doc.KandangID,
})
entry = &rows[len(rows)-1]
costMap[doc.ProjectFlockKandangID] = entry
costMap[doc.KandangID] = entry
}
entry.DocCost += doc.DocCost
entry.DocQty += doc.DocQty
if doc.SupplierID != nil {
if docSeen[doc.ProjectFlockKandangID] == nil {
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
if docSeen[doc.KandangID] == nil {
docSeen[doc.KandangID] = make(map[uint]bool)
}
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
if !docSeen[doc.KandangID][*doc.SupplierID] {
docSeen[doc.KandangID][*doc.SupplierID] = true
supplierName := ""
if doc.SupplierName != nil {
supplierName = *doc.SupplierName
@@ -252,19 +214,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
supplierAlias = *doc.SupplierAlias
}
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
ProjectFlockKandangID: doc.ProjectFlockKandangID,
SupplierID: *doc.SupplierID,
SupplierName: supplierName,
SupplierAlias: supplierAlias,
Category: "DOC",
KandangID: doc.KandangID,
SupplierID: *doc.SupplierID,
SupplierName: supplierName,
SupplierAlias: supplierAlias,
Category: "DOC",
})
}
}
}
budgetRows := make([]struct {
ProjectFlockKandangID uint
BudgetCost float64
KandangID uint
BudgetCost float64
}, 0)
pfkUsageSub := r.db.
@@ -285,63 +247,63 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
pfk.id AS project_flock_kandang_id,
k.id AS kandang_id,
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("pfk.id")
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("k.id")
budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
return nil, nil, err
}
for _, budget := range budgetRows {
entry, ok := costMap[budget.ProjectFlockKandangID]
entry, ok := costMap[budget.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: budget.ProjectFlockKandangID,
KandangID: budget.KandangID,
})
entry = &rows[len(rows)-1]
costMap[budget.ProjectFlockKandangID] = entry
costMap[budget.KandangID] = entry
}
entry.BudgetCost += budget.BudgetCost
}
expenseRows := make([]struct {
ProjectFlockKandangID uint
ExpenseCost float64
KandangID uint
ExpenseCost float64
}, 0)
expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
pfk.id AS project_flock_kandang_id,
k.id AS kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("pfk.id")
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("k.id")
expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
return nil, nil, err
}
for _, exp := range expenseRows {
entry, ok := costMap[exp.ProjectFlockKandangID]
entry, ok := costMap[exp.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: exp.ProjectFlockKandangID,
KandangID: exp.KandangID,
})
entry = &rows[len(rows)-1]
costMap[exp.ProjectFlockKandangID] = entry
costMap[exp.KandangID] = entry
}
entry.ExpenseCost += exp.ExpenseCost
}
@@ -350,7 +312,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
feedQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
@@ -361,21 +323,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
return nil, nil, err
}
for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
rows = append(rows, HppPerKandangCostRow{
ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID,
KandangID: feedSuppliers[i].KandangID,
})
costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1]
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
}
feedSuppliers[i].Category = "FEED"
}
@@ -385,67 +347,6 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
return rows, supplierRows, nil
}
func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
if len(projectFlockKandangIDs) == 0 {
return map[uint]HppPerKandangRow{}, nil
}
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
type eggRow struct {
ProjectFlockKandangID uint
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
}
eggRows := make([]eggRow, 0)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
r.project_flock_kandangs_id AS project_flock_kandang_id,
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("r.project_flock_kandangs_id")
if err := query.Scan(&eggRows).Error; err != nil {
return nil, err
}
result := make(map[uint]HppPerKandangRow, len(eggRows))
for _, row := range eggRows {
result[row.ProjectFlockKandangID] = HppPerKandangRow{
ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
EggProductionTotalPieces: row.EggProductionTotalPieces,
}
}
return result, nil
}
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
if len(areaIDs) > 0 {
query = query.Where("loc.area_id IN ?", areaIDs)
@@ -454,7 +355,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
query = query.Where("k.location_id IN ?", locationIDs)
}
if len(kandangIDs) > 0 {
query = query.Where("pfk.id IN ?", kandangIDs)
query = query.Where("k.id IN ?", kandangIDs)
}
return query
}
+1 -1
View File
@@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang)
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
}
@@ -2,7 +2,6 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
@@ -21,10 +20,9 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
@@ -44,25 +42,21 @@ type RepportService interface {
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
}
type repportService struct {
Log *logrus.Logger
Validate *validator.Validate
DB *gorm.DB
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
CustomerRepo customerRepo.CustomerRepository
Log *logrus.Logger
Validate *validator.Validate
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
}
@@ -77,7 +71,6 @@ type HppCostAggregate struct {
}
func NewRepportService(
db *gorm.DB,
validate *validator.Validate,
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
@@ -89,27 +82,22 @@ func NewRepportService(
debtSupplierRepo repportRepo.DebtSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository,
customerRepo customerRepo.CustomerRepository,
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
) RepportService {
return &repportService{
Log: utils.Log,
Validate: validate,
DB: db,
ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
CustomerRepo: customerRepo,
Log: utils.Log,
Validate: validate,
ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
}
@@ -320,15 +308,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
growthDetailCache := make(map[int]*entity.StandardGrowthDetail)
weeks := make([]int, len(weeklyResults))
for i := range weeklyResults {
weeks[i] = defaultStartWoa + i
}
uniformityMap, err := s.getUniformityByWeek(ctx.Context(), params.ProjectFlockKandangID, weeks)
if err != nil {
return nil, 0, err
}
var cumulativeButir int64
var cumulativeKg float64
for i := range weeklyResults {
@@ -338,12 +317,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
if weeklyResults[i].StdUniformity == "" {
weeklyResults[i].StdUniformity = defaultUniformText
}
if uniformity, ok := uniformityMap[defaultStartWoa+i]; ok {
weeklyResults[i].Uniformity = uniformity.Uniformity
if uniformity.AvgWeight != nil {
weeklyResults[i].Bw = *uniformity.AvgWeight
}
}
cumulativeButir += weeklyResults[i].ButiranJumlah
weeklyResults[i].TotalButir = cumulativeButir
@@ -417,205 +390,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
return weeklyResults, totalWeeks, nil
}
func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
// Determine customer IDs to process
var customerIDs []uint
var totalCustomers int64
if len(params.CustomerIDs) > 0 {
// Specific customer IDs mode (no pagination)
customerIDs = params.CustomerIDs
totalCustomers = int64(len(customerIDs))
if len(customerIDs) == 0 {
return []dto.CustomerPaymentReportItem{}, 0, nil
}
} else {
// Multiple customers mode with pagination
page := params.Page
limit := params.Limit
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
offset := (page - 1) * limit
var err error
customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset)
if err != nil {
return nil, 0, err
}
if len(customerIDs) == 0 {
return []dto.CustomerPaymentReportItem{}, 0, nil
}
}
var result []dto.CustomerPaymentReportItem
for _, customerID := range customerIDs {
item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
if err != nil {
return nil, 0, err
}
result = append(result, item)
}
return result, totalCustomers, nil
}
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) {
customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
cid := customerID
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
runningBalance := initialBalance
for i, tx := range transactions {
previousBalance := runningBalance
row := dto.ToCustomerPaymentReportRow(tx)
if tx.TransactionType == "SALES" {
runningBalance -= tx.TotalPrice
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
row.Status = status
if status == "LUNAS" {
if previousBalance >= tx.TotalPrice {
days := 0
row.AgingDay = &days
} else if paymentDate != nil {
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
} else {
days := 0
row.AgingDay = &days
}
} else {
days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
}
} else if tx.TransactionType == "PAYMENT" {
runningBalance += tx.PaymentAmount
row.Status = ""
row.AgingDay = nil
}
row.AccountsReceivable = runningBalance
rows = append(rows, row)
}
if params.StartDate != "" || params.EndDate != "" {
filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows))
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
var startDate, endDate *time.Time
if params.StartDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
startDate = &parsed
}
if params.EndDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location)
endDate = &endOfDay
}
for _, row := range rows {
transDate := row.TransDate.In(location)
if startDate != nil && transDate.Before(*startDate) {
continue
}
if endDate != nil && transDate.After(*endDate) {
continue
}
filteredRows = append(filteredRows, row)
}
rows = filteredRows
}
summary := dto.ToCustomerPaymentReportSummary(rows, initialBalance)
return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil
}
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
currentSales := transactions[currentIndex]
// Status Logic:
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
// 3. DIBAYAR SEBAGIAN: has payment but not enough
// 4. BELUM LUNAS: no payment at all
if previousBalance >= currentSales.TotalPrice {
return "LUNAS", nil
}
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
futureBalance := currentBalance
hasPayment := false
var paymentDateThatMadeItLunas *time.Time
for i := currentIndex + 1; i < len(transactions); i++ {
if transactions[i].TransactionType == "PAYMENT" {
futureBalance += transactions[i].PaymentAmount
hasPayment = true
if futureBalance >= 0 {
paymentDateThatMadeItLunas = &transactions[i].TransDate
return "LUNAS", paymentDateThatMadeItLunas
}
} else if transactions[i].TransactionType == "SALES" {
futureBalance -= transactions[i].TotalPrice
}
}
if hasPayment || hasPartialPaymentFromBalance {
return "DIBAYAR SEBAGIAN", nil
}
return "BELUM LUNAS", nil
}
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
result := dto.ProductionResultDTO{
CreatedAt: record.CreatedAt,
@@ -760,68 +534,6 @@ func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
return "", false
}
type uniformityWeekData struct {
Uniformity float64
AvgWeight *float64
}
type uniformityChartPayload struct {
Statistics *uniformityChartStats `json:"statistics"`
}
type uniformityChartStats struct {
AverageWeight *float64 `json:"average_weight"`
}
func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKandangID uint, weeks []int) (map[int]uniformityWeekData, error) {
result := make(map[int]uniformityWeekData, len(weeks))
if projectFlockKandangID == 0 || len(weeks) == 0 {
return result, nil
}
var rows []entity.ProjectFlockKandangUniformity
if err := s.DB.WithContext(ctx).
Model(&entity.ProjectFlockKandangUniformity{}).
Select("week, uniformity, uniform_date, id").
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("week IN ?", weeks).
Order("uniform_date DESC").
Order("id DESC").
Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if _, exists := result[row.Week]; exists {
continue
}
result[row.Week] = uniformityWeekData{
Uniformity: row.Uniformity,
AvgWeight: extractAverageWeight(row.ChartData, s.Log),
}
}
return result, nil
}
func extractAverageWeight(raw json.RawMessage, log *logrus.Logger) *float64 {
if len(raw) == 0 {
return nil
}
var payload uniformityChartPayload
if err := json.Unmarshal(raw, &payload); err != nil {
if log != nil {
log.WithError(err).Warn("uniformity chart_data decode failed")
}
return nil
}
if payload.Statistics == nil {
return nil
}
return payload.Statistics.AverageWeight
}
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
if groupSize <= 0 || len(daily) == 0 {
return daily
@@ -1345,42 +1057,13 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
if err != nil {
return nil, nil, err
}
validPfkIDs := make([]uint, 0, len(repoRows))
pfkIndex := make(map[uint]int, len(repoRows))
for idx := range repoRows {
row := repoRows[idx]
pfkIndex[row.ProjectFlockKandangID] = idx
if row.RecordingCount > 0 {
validPfkIDs = append(validPfkIDs, row.ProjectFlockKandangID)
}
costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
if err != nil {
return nil, nil, err
}
costRows := make([]repportRepo.HppPerKandangCostRow, 0)
supplierRows := make([]repportRepo.HppPerKandangSupplierRow, 0)
if len(validPfkIDs) > 0 {
costRows, supplierRows, err = s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
if err != nil {
return nil, nil, err
}
eggMap, err := s.HppPerKandangRepo.GetEggProductionByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
if err != nil {
return nil, nil, err
}
for pfkID, egg := range eggMap {
if rowIdx, ok := pfkIndex[pfkID]; ok {
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
}
}
}
costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows {
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
costMap[row.KandangID] = HppCostAggregate{
FeedCost: row.FeedCost,
OvkCost: row.OvkCost,
DocCost: row.DocCost,
@@ -1409,15 +1092,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
category = "DOC"
}
if seen[sup.ProjectFlockKandangID] == nil {
seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
if seen[sup.KandangID] == nil {
seen[sup.KandangID] = make(map[uint]bool)
}
if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
if seen[sup.KandangID][sup.SupplierID] {
continue
}
seen[sup.ProjectFlockKandangID][sup.SupplierID] = true
seen[sup.KandangID][sup.SupplierID] = true
targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
ID: int64(sup.SupplierID),
Name: sup.SupplierName,
Alias: sup.SupplierAlias,
@@ -1430,75 +1113,48 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
Max float64
}
type weightRangeAggregate struct {
Summary *dto.HppPerKandangSummaryWeightRangeDTO
RemainingBirds int64
RemainingWeightKg float64
AvgWeightSum float64
AvgWeightCount int64
EggHppSum float64
EggHppCount int
FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO
DocSuppliers map[int64]dto.HppPerKandangSupplierDTO
Summary *dto.HppPerKandangSummaryWeightRangeDTO
EggHppSum float64
EggHppCount int
}
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
var totalBirds int64
// var totalWeight float64
var totalWeight float64
var totalEggPieces int64
var totalEggKg float64
// var totalRemainingValueRp int64
var totalRemainingValueRp int64
var totalEggValueRp int64
// var totalHppSum float64
var totalHppSum float64
var totalHppCount int
var totalDocPriceSum float64
var totalDocPriceCount int
var totalEggHppSum float64
var totalEggHppCount int
var totalAvgWeightSum float64
var totalAvgWeightCount int64
for _, row := range repoRows {
if !params.ShowUnrecorded && row.RecordingCount == 0 {
continue
birdsFloat := row.RemainingChickenBirds
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
birdsFloat = 0
}
// birdsFloat := row.RemainingChickenBirds
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
// birdsFloat = 0
// }
// weightFloat := row.RemainingChickenWeight
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
// weightFloat = 0
// }
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
eggPiecesFloatRemaining = 0
weightFloat := row.RemainingChickenWeight
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
weightFloat = 0
}
eggTotalPiecesFloat := row.EggProductionTotalPieces
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
eggTotalPiecesFloat = 0
eggPiecesFloat := row.EggProductionPieces
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
eggPiecesFloat = 0
}
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
eggRemainingWeightFloatRemaining = 0
}
eggWeightFloat := row.EggProductionTotalWeightKg
eggWeightFloat := row.EggProductionWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0
}
avgWeight := 0.0
if eggTotalPiecesFloat > 0 {
avgWeight = eggWeightFloat / eggTotalPiecesFloat
if birdsFloat > 0 {
avgWeight = weightFloat / birdsFloat
}
if params.WeightMin != nil && avgWeight < *params.WeightMin {
continue
}
if params.WeightMax != nil && avgWeight > *params.WeightMax {
continue
}
weightMin := math.Floor(avgWeight*10) / 10
if weightMin < 0 {
weightMin = 0
@@ -1506,30 +1162,28 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
weightMax := weightMin + 0.09
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
// rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.ProjectFlockKandangID]
rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.KandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
// hppRp := 0.0
// if weightFloat > 0 {
// hppRp = totalCost / weightFloat
// }
hppRp := 0.0
if weightFloat > 0 {
hppRp = totalCost / weightFloat
}
eggHpp := 0.0
if eggWeightFloat > 0 {
eggHpp = (totalCost / eggWeightFloat) / 1000
eggHpp = totalCost / eggWeightFloat
}
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
// rowRemainingValue := int64(hppRp * weightFloat)
rowEggPieces := int64(math.Round(eggPiecesFloat))
rowEggValue := int64(eggHpp * eggWeightFloat)
rowRemainingValue := int64(hppRp * weightFloat)
avgDocPrice := int64(0)
if costEntry.DocQty > 0 {
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
}
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
ID: int(row.ProjectFlockKandangID),
ID: int(row.KandangID),
Kandang: dto.HppPerKandangRowKandangDTO{
ID: int64(row.KandangID),
Name: row.KandangName,
@@ -1547,35 +1201,32 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
WeightMin: weightMin,
WeightMax: weightMax,
},
AvgWeightKg: avgWeight,
NameWithPeriode: nameWithPeriod,
RemainingChickenBirds: rowBirds,
RemainingChickenWeightKg: weightFloat,
AvgWeightKg: avgWeight,
// FeedCostRp: costEntry.FeedCost,
// OvkCostRp: costEntry.OvkCost,
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
EggProductionKg: eggRemainingWeightFloatRemaining,
// EggProductionTotalWeightKg: eggWeightFloat,
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
AverageDocPriceRp: avgDocPrice,
// HppRp: hppRp,
EggHppRpPerKg: eggHpp,
// RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
DocSuppliers: docSupplierMap[row.KandangID],
FeedSuppliers: feedSupplierMap[row.KandangID],
EggProductionPieces: rowEggPieces,
EggProductionKg: eggWeightFloat,
AverageDocPriceRp: avgDocPrice,
HppRp: hppRp,
EggHppRpPerKg: eggHpp,
RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
})
// totalBirds += rowBirds
// totalWeight += weightFloat
totalBirds += rowBirds
totalWeight += weightFloat
totalEggPieces += rowEggPieces
totalEggKg += eggRemainingWeightFloatRemaining
// totalRemainingValueRp += rowRemainingValue
totalEggKg += eggWeightFloat
totalRemainingValueRp += rowRemainingValue
totalEggValueRp += rowEggValue
totalAvgWeightSum += avgWeight
totalAvgWeightCount++
// if weightFloat > 0 {
// totalHppSum += hppRp
// totalHppCount++
// }
if weightFloat > 0 {
totalHppSum += hppRp
totalHppCount++
}
if avgDocPrice > 0 {
totalDocPriceSum += float64(avgDocPrice)
totalDocPriceCount++
@@ -1595,30 +1246,16 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
},
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
},
FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
DocSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
}
perRangeMap[rangeKey] = rangeAgg
}
rangeSummary := rangeAgg.Summary
// rangeAgg.RemainingBirds += rowBirds
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
rangeAgg.AvgWeightSum += avgWeight
rangeAgg.AvgWeightCount++
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
rangeAgg.FeedSuppliers[supplier.ID] = supplier
}
}
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
rangeAgg.DocSuppliers[supplier.ID] = supplier
}
}
rangeSummary.RemainingChickenBirds += rowBirds
rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight
rangeSummary.EggProductionPieces += rowEggPieces
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
// rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggProductionKg += eggWeightFloat
rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggValueRp += rowEggValue
if eggWeightFloat > 0 {
rangeAgg.EggHppSum += eggHpp
@@ -1642,37 +1279,31 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
agg := perRangeMap[key]
entry := agg.Summary
entry.ID = idx + 1
if agg.AvgWeightCount > 0 {
entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount)
if entry.RemainingChickenBirds > 0 {
entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds)
}
if agg.EggHppCount > 0 {
entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
}
entry.FeedSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.FeedSuppliers))
for _, supplier := range agg.FeedSuppliers {
entry.FeedSuppliers = append(entry.FeedSuppliers, supplier)
}
entry.DocSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.DocSuppliers))
for _, supplier := range agg.DocSuppliers {
entry.DocSuppliers = append(entry.DocSuppliers, supplier)
}
perRangeSummary = append(perRangeSummary, *entry)
}
totalSummary := dto.HppPerKandangSummaryTotalDTO{
TotalEggProductionPieces: totalEggPieces,
TotalEggProductionKg: totalEggKg,
TotalEggValueRp: totalEggValueRp,
TotalRemainingChickenBirds: totalBirds,
TotalRemainingChickenWeightKg: totalWeight,
TotalEggProductionPieces: totalEggPieces,
TotalEggProductionKg: totalEggKg,
TotalRemainingValueRp: totalRemainingValueRp,
TotalEggValueRp: totalEggValueRp,
}
if totalBirds > 0 {
}
if totalAvgWeightCount > 0 {
totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount)
totalSummary.AverageWeightKg = totalWeight / float64(totalBirds)
}
if totalEggHppCount > 0 {
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
}
if totalHppCount > 0 {
totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount)
}
if totalDocPriceCount > 0 {
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
@@ -1765,9 +1396,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
}
params := &validation.HppPerKandangQuery{
Page: page,
@@ -71,11 +71,3 @@ type ProductionResultQuery struct {
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
}
type CustomerPaymentQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
}