mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eda50930e7 | |||
| 0bc5480a1d | |||
| ef482dd1b9 | |||
| 302f0ed877 | |||
| 31c48ee1da | |||
| 8ad11af9c9 | |||
| 688d3fa757 | |||
| 08be60c229 | |||
| 50b19dc1c3 | |||
| e770526c1a | |||
| 77af262662 | |||
| 21ff1c8ab7 | |||
| 2ca84ecffe | |||
| 4d334e8d5c | |||
| 62522a751f | |||
| 62ccc2e5d6 | |||
| 13fc246f21 | |||
| 89293a843e | |||
| 8792161c02 | |||
| c316a6d7a9 | |||
| 3a89e18b16 | |||
| c6dc94a4e1 | |||
| aeb5433346 | |||
| 804ff45dbd | |||
| 7daa509cd0 | |||
| f6e872c0aa | |||
| 7f1d796b65 | |||
| 6c7ff3f415 | |||
| bba2dec8c6 | |||
| f0b4fe916c |
@@ -52,6 +52,7 @@ const (
|
|||||||
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
||||||
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||||
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
||||||
|
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
SapronakService service.SapronakService
|
SapronakService service.SapronakService
|
||||||
|
ClosingKeuanganService service.ClosingKeuanganService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
SapronakService: sapronakService,
|
SapronakService: sapronakService,
|
||||||
|
ClosingKeuanganService: closingKeuanganService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +340,7 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -352,6 +354,34 @@ 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 {
|
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
|||||||
@@ -111,13 +111,13 @@ type ClosingPerformanceDTO struct {
|
|||||||
FeedIntake float64 `json:"feed_intake"`
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
FeedIntakeStd float64 `json:"feed_intake_std"`
|
FeedIntakeStd float64 `json:"feed_intake_std"`
|
||||||
HenDayAct *float64 `json:"hen_day_act,omitempty"`
|
HenDayAct *float64 `json:"hen_day_act,omitempty"`
|
||||||
HendayStd *float64 `json:"hen_day_std,omitempty"`
|
HendayStd float64 `json:"hen_day_std"`
|
||||||
EggMass *float64 `json:"egg_mass,omitempty"`
|
EggMass *float64 `json:"egg_mass,omitempty"`
|
||||||
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
|
EggMassStd float64 `json:"egg_mass_std"`
|
||||||
EggWeight *float64 `json:"egg_weight,omitempty"`
|
EggWeight *float64 `json:"egg_weight,omitempty"`
|
||||||
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
|
EggWeightStd float64 `json:"egg_weight_std"`
|
||||||
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
||||||
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
|
HenHouseStd float64 `json:"hen_housed_std"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
type ClosingSalesGroupDTO struct {
|
||||||
|
|||||||
@@ -1,135 +1,103 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
// === CLOSING KEUANGAN CODES ===
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
// Closing HPP Codes
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
type ClosingHPPCode string
|
||||||
)
|
|
||||||
|
|
||||||
// === CONSTANTS ===
|
|
||||||
const (
|
const (
|
||||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
HPPCodePakan ClosingHPPCode = "PAKAN"
|
||||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
HPPCodeOVK ClosingHPPCode = "OVK"
|
||||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
HPPCodeDOC ClosingHPPCode = "DOC"
|
||||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
|
||||||
HPPSummaryLabel = "HPP"
|
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
|
||||||
|
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
|
||||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
|
||||||
PLSalesTypeEgg = "Penjualan Telur"
|
|
||||||
|
|
||||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
|
||||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
|
||||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
|
||||||
|
|
||||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
|
||||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
|
||||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
|
||||||
|
|
||||||
PurchaseLabelPrefix = "Pembelian "
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// === CONTEXT STRUCTS ===
|
// Closing Profit Loss Codes
|
||||||
|
type ClosingProfitLossCode string
|
||||||
|
|
||||||
type CalculationContext struct {
|
const (
|
||||||
TotalPopulation float64
|
PLCodeSales ClosingProfitLossCode = "SALES"
|
||||||
TotalWeightProduced float64
|
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
|
||||||
TotalEggWeightKg float64
|
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
|
||||||
TotalDepletion float64
|
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
|
||||||
TotalWeightSold float64
|
)
|
||||||
ActualPopulation float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingKeuanganInput struct {
|
// === NEW CLOSING KEUANGAN DTO ===
|
||||||
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 {
|
type FinancialMetrics struct {
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Comparison struct {
|
// 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"`
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
Realization FinancialMetrics `json:"realization"`
|
Realization FinancialMetrics `json:"realization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HPP PURCHASES PACKAGE ===
|
// HPPSummary represents summary for HPP section
|
||||||
|
type HPPSummary struct {
|
||||||
type HppItem struct {
|
Label string `json:"label"`
|
||||||
Type string `json:"type"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
Comparison
|
Realization FinancialMetrics `json:"realization"`
|
||||||
|
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||||
|
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppGroup struct {
|
// HPPSection represents HPP data section
|
||||||
GroupName string `json:"group_name"`
|
type HPPSection struct {
|
||||||
Data []HppItem `json:"data"`
|
Items []HPPItem `json:"items"`
|
||||||
|
Summary HPPSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryHpp struct {
|
// ProfitLossItem represents an item in Profit & Loss section
|
||||||
Label string `json:"label"`
|
type ProfitLossItem struct {
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
|
||||||
Realization FinancialMetrics `json:"realization"`
|
Label string `json:"label"`
|
||||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
Type string `json:"type"` // "income", "purchase", "overhead"
|
||||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPurchasesSection struct {
|
// ProfitLossSummary represents summary for Profit & Loss section
|
||||||
Hpp []HppGroup `json:"hpp"`
|
type ProfitLossSummary struct {
|
||||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
GrossProfit FinancialMetrics `json:"gross_profit"`
|
||||||
}
|
SubTotal FinancialMetrics `json:"sub_total"`
|
||||||
|
NetProfit FinancialMetrics `json:"net_profit"`
|
||||||
// === 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 {
|
type ProfitLossSection struct {
|
||||||
Data ProfitLossData `json:"data"`
|
Items []ProfitLossItem `json:"items"`
|
||||||
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === RESPONSE DTO (ROOT) ===
|
// ClosingKeuanganData represents the main data structure
|
||||||
|
type ClosingKeuanganData struct {
|
||||||
|
HPP HPPSection `json:"hpp"`
|
||||||
|
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||||
|
}
|
||||||
|
|
||||||
type ReportResponse struct {
|
// ClosingKeuanganResponse represents the full API response
|
||||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
type ClosingKeuanganResponse struct {
|
||||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data ClosingKeuanganData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MAPPER FUNCTIONS ===
|
// === MAPPER FUNCTIONS ===
|
||||||
|
|
||||||
|
// ToFinancialMetrics creates FinancialMetrics from values
|
||||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||||
return FinancialMetrics{
|
return FinancialMetrics{
|
||||||
RpPerBird: rpPerBird,
|
RpPerBird: rpPerBird,
|
||||||
@@ -138,451 +106,80 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
// ToHPPItem creates HPP item
|
||||||
return Comparison{
|
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
|
||||||
|
return HPPItem{
|
||||||
|
ID: id,
|
||||||
|
Category: category,
|
||||||
|
Code: code,
|
||||||
|
Label: label,
|
||||||
Budgeting: budgeting,
|
Budgeting: budgeting,
|
||||||
Realization: realization,
|
Realization: realization,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
// ToHPPSummary creates HPP summary
|
||||||
|
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
|
||||||
func getFlagLabel(flagType utils.FlagType) string {
|
return HPPSummary{
|
||||||
return PurchaseLabelPrefix + string(flagType)
|
Label: label,
|
||||||
}
|
Budgeting: budgeting,
|
||||||
|
Realization: realization,
|
||||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
EggBudgeting: eggBudgeting,
|
||||||
flags := []utils.FlagType{
|
EggRealization: eggRealization,
|
||||||
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),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
// ToHPPSection creates HPP section
|
||||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
|
||||||
|
return HPPSection{
|
||||||
return HppItem{
|
Items: items,
|
||||||
Type: HPPLabelEkspedisi,
|
Summary: summary,
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
// ToProfitLossItem creates Profit & Loss item
|
||||||
items := []HppItem{}
|
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
|
||||||
|
return ProfitLossItem{
|
||||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
Code: code,
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
Label: label,
|
||||||
|
Type: itemType,
|
||||||
if budgetAmount > 0 || realizationAmount > 0 {
|
RpPerBird: rpPerBird,
|
||||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
RpPerKg: rpPerKg,
|
||||||
}
|
Amount: amount,
|
||||||
|
|
||||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
|
||||||
|
|
||||||
return HppGroup{
|
|
||||||
GroupName: HPPGroupBahanBaku,
|
|
||||||
Data: items,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === HPP SUMMARY ===
|
// ToProfitLossSummary creates Profit & Loss summary
|
||||||
|
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
|
||||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
|
return ProfitLossSummary{
|
||||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
GrossProfit: grossProfit,
|
||||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
SubTotal: subTotal,
|
||||||
totalBudget := purchaseTotal + budgetTotal
|
NetProfit: netProfit,
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === PROFIT & LOSS ===
|
// ToProfitLossSection creates Profit & Loss section
|
||||||
|
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
|
||||||
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{
|
return ProfitLossSection{
|
||||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
Items: items,
|
||||||
|
Summary: summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
// ToClosingKeuanganData creates complete closing keuangan data
|
||||||
totalAmount, totalPerBird := sumPLItems(items)
|
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
|
||||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
return ClosingKeuanganData{
|
||||||
}
|
HPP: hpp,
|
||||||
|
ProfitLoss: profitLoss,
|
||||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
|
||||||
return ReportResponse{
|
|
||||||
HppPurchases: hppPurchases,
|
|
||||||
ProfitLoss: profitLoss,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
// ToSuccessClosingKeuanganResponse creates success response
|
||||||
var totalPopulation float64
|
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
|
||||||
var totalWeightSold float64
|
return ClosingKeuanganResponse{
|
||||||
|
Code: 200,
|
||||||
for _, chickin := range input.Chickins {
|
Status: "success",
|
||||||
totalPopulation += chickin.UsageQty
|
Message: "Get closing keuangan successfully",
|
||||||
}
|
Data: data,
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type ClosingModule struct{}
|
|||||||
|
|
||||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
|
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
@@ -41,8 +42,9 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
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)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type ClosingRepository interface {
|
|||||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, 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)
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
|
|
||||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +317,7 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo
|
|||||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||||
Where("e.category = ?", "BOP").
|
Where("e.category = ?", "BOP").
|
||||||
|
Where("e.realization_date IS NOT NULL").
|
||||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
||||||
|
|
||||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
||||||
@@ -473,8 +473,8 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
COALESCE(fw.name, '') AS source_warehouse,
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
'' AS destination_warehouse,
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
COALESCE(tw.name, '') AS destination,
|
'' AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Transfer to other unit' AS notes
|
'Transfer to other unit' AS notes
|
||||||
@@ -522,8 +522,8 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
w.name AS source_warehouse,
|
w.name AS source_warehouse,
|
||||||
'' AS destination_warehouse,
|
'RETAIL CUSTOMER' AS destination_warehouse,
|
||||||
'RETAIL CUSTOMER' AS destination,
|
'' AS destination,
|
||||||
mp.qty AS quantity,
|
mp.qty AS quantity,
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
m.notes AS notes
|
m.notes AS notes
|
||||||
@@ -889,143 +889,58 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActualUsageCostRow struct {
|
outgoingQuery := r.withCtx(ctx).
|
||||||
ProductID uint `gorm:"column:product_id"`
|
Table("stock_allocations AS sa").
|
||||||
ProductName string `gorm:"column:product_name"`
|
|
||||||
FlagName string `gorm:"column:flag_name"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
AveragePrice float64 `gorm:"column:average_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return []ActualUsageCostRow{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
db := r.DB().WithContext(ctx)
|
|
||||||
|
|
||||||
// Get all project flock kandang IDs for this project flock
|
|
||||||
var pfkIDs []uint
|
|
||||||
err := db.Table("project_flock_kandangs").
|
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
|
||||||
Pluck("id", &pfkIDs).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pfkIDs) == 0 {
|
|
||||||
return []ActualUsageCostRow{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []ActualUsageCostRow
|
|
||||||
|
|
||||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
|
||||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
|
||||||
|
|
||||||
recordingQuery := db.
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select(`
|
Select(`
|
||||||
pw.product_id AS product_id,
|
std.product_id AS product_id,
|
||||||
p.name AS product_name,
|
p.name AS product_name,
|
||||||
COALESCE(f.name, tf.name) AS flag_name,
|
f.name AS flag,
|
||||||
COALESCE(SUM(
|
st.transfer_date::timestamp AS date,
|
||||||
CASE
|
COALESCE(st.movement_number, '') AS reference,
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
0 AS qty_in,
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||||
ELSE 0
|
COALESCE(p.product_price, 0) AS price
|
||||||
END
|
|
||||||
), 0) AS total_qty,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) AS total_price,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) AS qty_divisor,
|
|
||||||
COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0) / NULLIF(COALESCE(SUM(
|
|
||||||
CASE
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
|
||||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
), 0), 0) AS average_price`,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey,
|
|
||||||
purchaseStockableKey, transferStockableKey).
|
|
||||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
|
||||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
|
||||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
|
||||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
|
|
||||||
"recording_stocks", entity.StockAllocationStatusActive).
|
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
|
|
||||||
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
|
|
||||||
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 ?", pfkIDs).
|
|
||||||
Where("r.deleted_at IS NULL").
|
|
||||||
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
|
|
||||||
|
|
||||||
if err := recordingQuery.Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
chickinQuery := db.
|
|
||||||
Table("project_chickins AS pc").
|
|
||||||
Select(`
|
|
||||||
pw.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag_name,
|
|
||||||
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
|
|
||||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
|
|
||||||
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
|
|
||||||
`).
|
`).
|
||||||
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
|
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
|
||||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
Where("pc.usage_qty > 0").
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Group("pw.product_id, p.name, f.name")
|
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
var chickinRows []ActualUsageCostRow
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
|
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
||||||
return nil, err
|
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = append(rows, chickinRows...)
|
return incoming, outgoing, nil
|
||||||
|
|
||||||
return rows, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClosingKeuanganRepository handles database operations for closing keuangan
|
||||||
|
type ClosingKeuanganRepository interface {
|
||||||
|
repository.BaseRepository[interface{}]
|
||||||
|
|
||||||
|
// All Product Usage
|
||||||
|
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error)
|
||||||
|
|
||||||
|
// Depletion per kandang
|
||||||
|
GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
|
||||||
|
// Weight produced from uniformity per kandang
|
||||||
|
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
|
||||||
|
// DB returns the underlying GORM DB instance
|
||||||
|
DB() *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingKeuanganRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[interface{}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository {
|
||||||
|
return &ClosingKeuanganRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result Rows
|
||||||
|
|
||||||
|
type ProductUsageRow struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
FlagNames string `gorm:"column:flag_names"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
|
||||||
|
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
|
||||||
|
// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return []ProductUsageRow{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubQueryResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AggregatedResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
PriceCount int `gorm:"-"` // For calculating average price
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlagResult struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
FlagNames string `gorm:"column:flag_names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allResults []SubQueryResult
|
||||||
|
|
||||||
|
// Subquery 1: Recordings
|
||||||
|
var recordingsResults []SubQueryResult
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recordings r").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(CASE "+
|
||||||
|
"WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0) "+
|
||||||
|
"WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0) "+
|
||||||
|
"ELSE 0 END), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) as price").
|
||||||
|
Joins("JOIN recording_stocks rs ON rs.recording_id = r.id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'").
|
||||||
|
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'").
|
||||||
|
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'").
|
||||||
|
Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'").
|
||||||
|
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'").
|
||||||
|
Where("r.project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||||
|
Where("r.deleted_at IS NULL").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&recordingsResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get recordings product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, recordingsResults...)
|
||||||
|
|
||||||
|
// Subquery 2: Chickins
|
||||||
|
var chickinsResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("project_chickins pc").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(pc.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("pc.usage_qty > 0").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&chickinsResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get chickins product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, chickinsResults...)
|
||||||
|
|
||||||
|
// Subquery 3: Marketing Delivery
|
||||||
|
var marketingResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("marketing_delivery_products mdp").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(mdp.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&marketingResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get marketing product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, marketingResults...)
|
||||||
|
|
||||||
|
// Subquery 4: Laying Transfer Sources
|
||||||
|
var layingTransferResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("laying_transfer_sources lts").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(lts.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&layingTransferResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, layingTransferResults...)
|
||||||
|
|
||||||
|
// Subquery 5: Stock Transfer Details
|
||||||
|
var stockTransferResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("stock_transfer_details std").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(std.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = std.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&stockTransferResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, stockTransferResults...)
|
||||||
|
|
||||||
|
// Subquery 6: Adjustment Stocks
|
||||||
|
var adjustmentResults []SubQueryResult
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("adjustment_stocks ads").
|
||||||
|
Select("pw.product_id, p.name as product_name, "+
|
||||||
|
"COALESCE(SUM(ads.usage_qty), 0) as total_qty, "+
|
||||||
|
"COALESCE(AVG(COALESCE(pi.price, 0)), 0) as price").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id").
|
||||||
|
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("ads.usage_qty > 0").
|
||||||
|
Group("pw.product_id, p.name").
|
||||||
|
Scan(&adjustmentResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get adjustment product usage: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID)
|
||||||
|
allResults = append(allResults, adjustmentResults...)
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults))
|
||||||
|
|
||||||
|
// Aggregate results by product_id
|
||||||
|
aggregatedMap := make(map[uint]*AggregatedResult)
|
||||||
|
for _, result := range allResults {
|
||||||
|
key := result.ProductID
|
||||||
|
if existing, exists := aggregatedMap[key]; exists {
|
||||||
|
existing.TotalQty += result.TotalQty
|
||||||
|
existing.Price += result.Price
|
||||||
|
existing.PriceCount++
|
||||||
|
} else {
|
||||||
|
aggregatedMap[key] = &AggregatedResult{
|
||||||
|
ProductID: result.ProductID,
|
||||||
|
ProductName: result.ProductName,
|
||||||
|
TotalQty: result.TotalQty,
|
||||||
|
Price: result.Price,
|
||||||
|
PriceCount: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap))
|
||||||
|
|
||||||
|
// Get flags for all products
|
||||||
|
productIDs := make([]uint, 0, len(aggregatedMap))
|
||||||
|
for id := range aggregatedMap {
|
||||||
|
productIDs = append(productIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagResults []FlagResult
|
||||||
|
if len(productIDs) > 0 {
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("products p").
|
||||||
|
Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names").
|
||||||
|
Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id").
|
||||||
|
Where("p.id IN ?", productIDs).
|
||||||
|
Group("p.id").
|
||||||
|
Scan(&flagResults).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product flags: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults))
|
||||||
|
|
||||||
|
// Build flag map
|
||||||
|
flagMap := make(map[uint]string)
|
||||||
|
for _, flag := range flagResults {
|
||||||
|
flagMap[flag.ProductID] = flag.FlagNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine results and calculate average price
|
||||||
|
results := make([]ProductUsageRow, 0, len(aggregatedMap))
|
||||||
|
for _, agg := range aggregatedMap {
|
||||||
|
avgPrice := float64(0)
|
||||||
|
if agg.PriceCount > 0 {
|
||||||
|
avgPrice = agg.Price / float64(agg.PriceCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
flagNames := flagMap[agg.ProductID]
|
||||||
|
|
||||||
|
// Apply flag filters if provided
|
||||||
|
if len(flagFilters) > 0 {
|
||||||
|
// Check if any of the flagFilters exist in flagNames
|
||||||
|
matched := false
|
||||||
|
for _, filter := range flagFilters {
|
||||||
|
if containsIgnoreCase(flagNames, filter) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
continue // Skip this product if no flag matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, ProductUsageRow{
|
||||||
|
ProductID: agg.ProductID,
|
||||||
|
ProductName: agg.ProductName,
|
||||||
|
FlagNames: flagNames,
|
||||||
|
TotalQty: agg.TotalQty,
|
||||||
|
Price: avgPrice,
|
||||||
|
TotalPengeluaran: agg.TotalQty * avgPrice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results))
|
||||||
|
for i, r := range results {
|
||||||
|
fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n",
|
||||||
|
i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by product name
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].ProductName < results[j].ProductName
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("[REPO] Final sorted results: %d items\n", len(results))
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
var result float64
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_depletions").
|
||||||
|
Select("COALESCE(SUM(recording_depletions.qty), 0)").
|
||||||
|
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
||||||
|
Where("project_flock_kandangs.id = ?", projectFlockKandangID).
|
||||||
|
Scan(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang
|
||||||
|
// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||||
|
func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniformity struct {
|
||||||
|
MeanUp float64
|
||||||
|
ChickQtyOfWeight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("project_flock_kandang_uniformity").
|
||||||
|
Select("mean_up, chick_qty_of_weight").
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&uniformity).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000
|
||||||
|
totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000
|
||||||
|
|
||||||
|
return totalWeight, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsIgnoreCase checks if a string contains a substring (case-insensitive)
|
||||||
|
func containsIgnoreCase(str, substr string) bool {
|
||||||
|
return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr))
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
|
||||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
@@ -34,5 +34,6 @@ 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("/: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/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
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,7 +40,6 @@ type ClosingService interface {
|
|||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, 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)
|
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)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,8 +336,10 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
var projectFlockKandangIDs []uint
|
||||||
if params.Type == validation.SapronakTypeOutgoing {
|
if params.KandangID != nil && *params.KandangID > 0 {
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
|
projectFlockKandangIDs = []uint{*params.KandangID}
|
||||||
|
} else if params.Type == validation.SapronakTypeOutgoing {
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
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")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
@@ -418,14 +419,11 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
var ids []uint
|
var ids []uint
|
||||||
query := s.Repository.DB().WithContext(ctx).
|
query := s.Repository.DB().WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockKandang{}).
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
Where("project_flock_id = ?", projectFlockID)
|
Where("project_flock_id = ?", projectFlockID)
|
||||||
if kandangID != nil {
|
|
||||||
query = query.Where("kandang_id = ?", *kandangID)
|
|
||||||
}
|
|
||||||
err := query.Order("id ASC").Pluck("id", &ids).Error
|
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -577,82 +575,6 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
|
||||||
|
|
||||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("MarketingProduct").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
|
||||||
})
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
|
||||||
}
|
|
||||||
|
|
||||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return &report, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
@@ -690,10 +612,16 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
|
var projectFlockKandangIDs []uint
|
||||||
if err != nil {
|
if kandangID != nil && *kandangID > 0 {
|
||||||
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
|
projectFlockKandangIDs = []uint{*kandangID}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
|
} 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
@@ -954,16 +882,16 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
}
|
}
|
||||||
if !isGrowing {
|
if !isGrowing {
|
||||||
if productionStandardDetail.TargetHenDayProduction != nil {
|
if productionStandardDetail.TargetHenDayProduction != nil {
|
||||||
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
|
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetHenHouseProduction != nil {
|
if productionStandardDetail.TargetHenHouseProduction != nil {
|
||||||
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
|
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetEggWeight != nil {
|
if productionStandardDetail.TargetEggWeight != nil {
|
||||||
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
|
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetEggMass != nil {
|
if productionStandardDetail.TargetEggMass != nil {
|
||||||
performance.EggMassStd = productionStandardDetail.TargetEggMass
|
performance.EggMassStd = *productionStandardDetail.TargetEggMass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1103,52 +1031,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
|||||||
return closest.Mortality, closest.FcrNumber
|
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.Repository.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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,640 @@
|
|||||||
|
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,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -112,7 +111,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
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")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
@@ -126,8 +125,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
KandangName: pfk.Kandang.Name,
|
KandangName: pfk.Kandang.Name,
|
||||||
Period: pfk.Period,
|
Period: pfk.Period,
|
||||||
Status: status,
|
Status: status,
|
||||||
StartDate: nil,
|
|
||||||
EndDate: nil,
|
|
||||||
TotalIncomingValue: totalIncoming,
|
TotalIncomingValue: totalIncoming,
|
||||||
TotalUsageValue: totalUsage,
|
TotalUsageValue: totalUsage,
|
||||||
Items: items,
|
Items: items,
|
||||||
@@ -318,7 +315,7 @@ func buildSapronakDetails(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
// For sapronak closing report we intentionally ignore date range
|
// For sapronak closing report we intentionally ignore date range
|
||||||
// and aggregate all historical transactions for the kandang/project.
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
@@ -419,6 +416,22 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
return groupMap[flag]
|
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 {
|
for _, row := range incoming {
|
||||||
if !matchesFlag(row.Flag) {
|
if !matchesFlag(row.Flag) {
|
||||||
continue
|
continue
|
||||||
@@ -554,19 +567,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range incomingDetails {
|
for productID, details := range incomingDetails {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -575,19 +587,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjIncoming {
|
for productID, details := range adjIncoming {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -596,19 +607,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range usageDetails {
|
for productID, details := range usageDetails {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -616,19 +626,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjOutgoing {
|
for productID, details := range adjOutgoing {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -636,19 +645,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transIncoming {
|
for productID, details := range transIncoming {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -657,19 +665,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transOutgoing {
|
for productID, details := range transOutgoing {
|
||||||
flag := ""
|
flag, name := resolveFlagName(productID, details)
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
d.Flag = flag
|
if d.Flag == "" {
|
||||||
d.ProductName = name
|
d.Flag = flag
|
||||||
|
}
|
||||||
|
if d.ProductName == "" {
|
||||||
|
d.ProductName = name
|
||||||
|
}
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= 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
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
totalPrice := requestedProduct.UnitPrice * totalWeight
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
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
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
totalPrice := requestedProduct.UnitPrice * totalWeight
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
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
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * rp.Qty
|
totalPrice := rp.UnitPrice * totalWeight
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"product_warehouse_id": rp.ProductWarehouseId,
|
"product_warehouse_id": rp.ProductWarehouseId,
|
||||||
@@ -594,7 +594,7 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
|
|||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * rp.Qty
|
totalPrice := rp.UnitPrice * totalWeight
|
||||||
|
|
||||||
marketingProduct := &entity.MarketingProduct{
|
marketingProduct := &entity.MarketingProduct{
|
||||||
MarketingId: marketingId,
|
MarketingId: marketingId,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type CustomerRelationDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
AccountNumber string `json:"account_number"`
|
AccountNumber string `json:"account_number"`
|
||||||
|
Address string `json:"address,omitempty"`
|
||||||
Balance float64 `json:"balance"`
|
Balance float64 `json:"balance"`
|
||||||
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
|
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,8 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
|
|||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Type: e.Type,
|
Type: e.Type,
|
||||||
AccountNumber: e.AccountNumber,
|
AccountNumber: e.AccountNumber,
|
||||||
|
Address: e.Address,
|
||||||
|
Balance: e.Balance,
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
Category: c.Query("category", ""),
|
Category: c.Query("category", ""),
|
||||||
|
Flag: c.Query("flag", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ func (s supplierService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("NonstockSuppliers.Nonstock.Flags")
|
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) {
|
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -63,7 +67,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withListRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,23 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
|
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
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")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1"`
|
||||||
|
Flag string `query:"flag" validate:"omitempty"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
Category string `query:"category" validate:"omitempty,max=50"`
|
Category string `query:"category" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,11 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
typ := strings.ToUpper(req.Type)
|
typ := strings.ToUpper(req.Type)
|
||||||
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
|
createValidationOpts := WarehouseTypeValidationOptions{
|
||||||
|
LocationProvided: req.LocationId != nil,
|
||||||
|
KandangProvided: req.KandangId != nil,
|
||||||
|
}
|
||||||
|
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +212,22 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
finalKandangId = req.KandangId
|
finalKandangId = req.KandangId
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
|
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 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if originalLocationId != finalLocationId {
|
||||||
|
updateBody["location_id"] = nil
|
||||||
|
}
|
||||||
|
if originalKandangId != finalKandangId {
|
||||||
|
updateBody["kandang_id"] = nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
@@ -238,47 +255,65 @@ func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
|
type WarehouseTypeValidationOptions struct {
|
||||||
|
AutoClear bool
|
||||||
|
LocationProvided bool
|
||||||
|
KandangProvided bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID **uint, kandangID **uint, opts WarehouseTypeValidationOptions) error {
|
||||||
switch utils.WarehouseType(typ) {
|
switch utils.WarehouseType(typ) {
|
||||||
case utils.WarehouseTypeArea:
|
case utils.WarehouseTypeArea:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
|
||||||
}
|
}
|
||||||
if locationID != nil {
|
if locationID != nil && *locationID != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
|
if opts.AutoClear && !opts.LocationProvided {
|
||||||
|
*locationID = nil
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if kandangID != nil {
|
if kandangID != nil && *kandangID != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
|
if opts.AutoClear && !opts.KandangProvided {
|
||||||
|
*kandangID = nil
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case utils.WarehouseTypeLokasi:
|
case utils.WarehouseTypeLokasi:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
|
||||||
}
|
}
|
||||||
if locationID == nil {
|
if locationID == nil || *locationID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
|
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")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
|
||||||
}
|
}
|
||||||
if kandangID != nil {
|
if kandangID != nil && *kandangID != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
|
if opts.AutoClear && !opts.KandangProvided {
|
||||||
|
*kandangID = nil
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case utils.WarehouseTypeKandang:
|
case utils.WarehouseTypeKandang:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
|
||||||
}
|
}
|
||||||
if locationID == nil {
|
if locationID == nil || *locationID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
|
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")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
|
||||||
}
|
}
|
||||||
if kandangID == nil {
|
if kandangID == nil || *kandangID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
|
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 fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -584,7 +584,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return updated, nil
|
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 {
|
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
|
||||||
if s.ProductRepo == nil {
|
if s.ProductRepo == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type RecordingRepository interface {
|
|||||||
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
||||||
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
||||||
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, 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)
|
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
||||||
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
||||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||||
@@ -548,3 +549,30 @@ func nextRecordingDay(days []int) int {
|
|||||||
|
|
||||||
return len(normalized) + 1
|
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,6 +36,7 @@ type ExpenseReceivingPayload struct {
|
|||||||
TransportPerItem *float64
|
TransportPerItem *float64
|
||||||
ReceivedQty float64
|
ReceivedQty float64
|
||||||
ReceivedDate *time.Time
|
ReceivedDate *time.Time
|
||||||
|
VehicleNumber *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type groupedItem struct {
|
type groupedItem struct {
|
||||||
@@ -182,6 +183,22 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
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.
|
// Load current links to decide whether to update in place or recreate.
|
||||||
type itemLink struct {
|
type itemLink struct {
|
||||||
@@ -205,9 +222,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
|
|
||||||
itemLinks := make(map[uint]itemLink)
|
itemLinks := make(map[uint]itemLink)
|
||||||
updatedExpenses := make(map[uint64]struct{})
|
updatedExpenses := make(map[uint64]struct{})
|
||||||
if len(updates) > 0 {
|
if len(filtered) > 0 {
|
||||||
ids := make([]uint, 0, len(updates))
|
ids := make([]uint, 0, len(filtered))
|
||||||
for _, upd := range updates {
|
for _, upd := range filtered {
|
||||||
if upd.PurchaseItemID != 0 {
|
if upd.PurchaseItemID != 0 {
|
||||||
ids = append(ids, upd.PurchaseItemID)
|
ids = append(ids, upd.PurchaseItemID)
|
||||||
}
|
}
|
||||||
@@ -252,7 +269,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
|
|
||||||
groups := make(map[string][]groupedItem)
|
groups := make(map[string][]groupedItem)
|
||||||
|
|
||||||
for _, payload := range updates {
|
for _, payload := range filtered {
|
||||||
if payload.ReceivedDate == nil {
|
if payload.ReceivedDate == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -702,6 +702,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
warehouseID uint
|
warehouseID uint
|
||||||
supplierID uint
|
supplierID uint
|
||||||
transportPerItem *float64
|
transportPerItem *float64
|
||||||
|
vehicleNumber *string
|
||||||
overrideWarehouse bool
|
overrideWarehouse bool
|
||||||
receivedQty float64
|
receivedQty float64
|
||||||
}
|
}
|
||||||
@@ -756,7 +757,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
}
|
}
|
||||||
visitedItems[payload.PurchaseItemID] = struct{}{}
|
visitedItems[payload.PurchaseItemID] = struct{}{}
|
||||||
|
|
||||||
supplierID := purchase.SupplierId
|
var supplierID uint
|
||||||
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
|
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
|
||||||
supplierID = *payload.ExpeditionVendorID
|
supplierID = *payload.ExpeditionVendorID
|
||||||
}
|
}
|
||||||
@@ -770,6 +771,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
transportPerItem = &val
|
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{
|
prepared = append(prepared, preparedReceiving{
|
||||||
item: item,
|
item: item,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -777,6 +787,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
warehouseID: warehouseID,
|
warehouseID: warehouseID,
|
||||||
supplierID: supplierID,
|
supplierID: supplierID,
|
||||||
transportPerItem: transportPerItem,
|
transportPerItem: transportPerItem,
|
||||||
|
vehicleNumber: vehicleNumber,
|
||||||
overrideWarehouse: overrideWarehouse,
|
overrideWarehouse: overrideWarehouse,
|
||||||
receivedQty: receivedQty,
|
receivedQty: receivedQty,
|
||||||
})
|
})
|
||||||
@@ -964,6 +975,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
TransportPerItem: prep.transportPerItem,
|
TransportPerItem: prep.transportPerItem,
|
||||||
ReceivedQty: prep.receivedQty,
|
ReceivedQty: prep.receivedQty,
|
||||||
ReceivedDate: &date,
|
ReceivedDate: &date,
|
||||||
|
VehicleNumber: prep.vehicleNumber,
|
||||||
}
|
}
|
||||||
receivingPayloads = append(receivingPayloads, payload)
|
receivingPayloads = append(receivingPayloads, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,65 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(resp)
|
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 {
|
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
|
||||||
idParam := ctx.Params("idProjectFlockKandang")
|
idParam := ctx.Params("idProjectFlockKandang")
|
||||||
if idParam == "" {
|
if idParam == "" {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -25,14 +25,12 @@ type HppPerKandangResponseData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRowDTO struct {
|
type HppPerKandangRowDTO struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
||||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||||
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
|
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||||
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
|
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
EggProductionKg float64 `json:"egg_production_kg"`
|
||||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
|
||||||
EggProductionKg float64 `json:"egg_production_kg"`
|
|
||||||
// FeedCostRp float64 `json:"feed_cost_rp"`
|
// FeedCostRp float64 `json:"feed_cost_rp"`
|
||||||
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
||||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||||
@@ -80,34 +78,28 @@ type HppPerKandangSummaryDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSummaryWeightRangeDTO struct {
|
type HppPerKandangSummaryWeightRangeDTO struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
|
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||||
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
|
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
EggProductionKg float64 `json:"egg_production_kg"`
|
||||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||||
EggProductionKg float64 `json:"egg_production_kg"`
|
EggValueRp int64 `json:"egg_value_rp"`
|
||||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
||||||
EggValueRp int64 `json:"egg_value_rp"`
|
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
||||||
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
|
||||||
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
HppRp float64 `json:"hpp_rp"`
|
||||||
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
|
RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||||
HppRp float64 `json:"hpp_rp"`
|
|
||||||
RemainingValueRp int64 `json:"remaining_value_rp"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSummaryTotalDTO struct {
|
type HppPerKandangSummaryTotalDTO struct {
|
||||||
TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"`
|
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||||
TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"`
|
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
|
||||||
AverageWeightKg float64 `json:"average_weight_kg"`
|
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
|
||||||
TotalRemainingValueRp int64 `json:"total_remaining_value_rp"`
|
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
|
||||||
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
|
TotalEggValueRp int64 `json:"total_egg_value_rp"`
|
||||||
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
|
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
|
||||||
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 {
|
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
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"
|
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"
|
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
@@ -35,26 +36,14 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
||||||
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
||||||
productionResultRepository := repportRepo.NewProductionResultRepository(db)
|
productionResultRepository := repportRepo.NewProductionResultRepository(db)
|
||||||
|
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
|
||||||
|
customerRepository := customerRepo.NewCustomerRepository(db)
|
||||||
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
|
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
|
||||||
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
|
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
|
||||||
userRepository := rUser.NewUserRepository(db)
|
userRepository := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
||||||
repportService := sRepport.NewRepportService(
|
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
|
||||||
validate,
|
|
||||||
expenseRealizationRepository,
|
|
||||||
marketingDeliveryProductRepository,
|
|
||||||
purchaseRepository,
|
|
||||||
chickinRepository,
|
|
||||||
recordingRepository,
|
|
||||||
approvalSvc,
|
|
||||||
purchaseSupplierRepository,
|
|
||||||
debtSupplierRepository,
|
|
||||||
hppPerKandangRepository,
|
|
||||||
productionResultRepository,
|
|
||||||
standardGrowthDetailRepository,
|
|
||||||
productionStandardDetailRepository,
|
|
||||||
)
|
|
||||||
userService := sUser.NewUserService(userRepository, validate)
|
userService := sUser.NewUserService(userRepository, validate)
|
||||||
|
|
||||||
RepportRoutes(router, userService, repportService)
|
RepportRoutes(router, userService, repportService)
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
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,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HppPerKandangRow struct {
|
type HppPerKandangRow struct {
|
||||||
|
ProjectFlockKandangID uint
|
||||||
KandangID uint
|
KandangID uint
|
||||||
KandangName string
|
KandangName string
|
||||||
KandangStatus string
|
KandangStatus string
|
||||||
@@ -18,6 +19,7 @@ type HppPerKandangRow struct {
|
|||||||
LocationName string
|
LocationName string
|
||||||
PicID uint
|
PicID uint
|
||||||
PicName string
|
PicName string
|
||||||
|
RecordingCount int64
|
||||||
RemainingChickenBirds float64
|
RemainingChickenBirds float64
|
||||||
RemainingChickenWeight float64
|
RemainingChickenWeight float64
|
||||||
EggProductionWeightKg float64
|
EggProductionWeightKg float64
|
||||||
@@ -44,7 +46,8 @@ type HppPerKandangSupplierRow struct {
|
|||||||
|
|
||||||
type HppPerKandangRepository interface {
|
type HppPerKandangRepository interface {
|
||||||
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
||||||
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type hppPerKandangRepository struct {
|
type hppPerKandangRepository struct {
|
||||||
@@ -58,9 +61,31 @@ func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository {
|
|||||||
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
|
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
|
||||||
var rows []HppPerKandangRow
|
var rows []HppPerKandangRow
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
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").
|
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").
|
||||||
Select(`
|
Select(`
|
||||||
|
pfk.id AS project_flock_kandang_id,
|
||||||
k.id AS kandang_id,
|
k.id AS kandang_id,
|
||||||
k.name AS kandang_name,
|
k.name AS kandang_name,
|
||||||
k.status AS kandang_status,
|
k.status AS kandang_status,
|
||||||
@@ -68,22 +93,21 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
loc.name AS location_name,
|
loc.name AS location_name,
|
||||||
pic.id AS pic_id,
|
pic.id AS pic_id,
|
||||||
pic.name AS pic_name,
|
pic.name AS pic_name,
|
||||||
COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds,
|
COALESCE(COUNT(vr.id), 0) AS recording_count,
|
||||||
COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight,
|
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
|
||||||
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
|
0 AS remaining_chicken_weight,
|
||||||
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
|
0 AS egg_production_weight_kg,
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
0 AS egg_production_pieces`).
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_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 locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
||||||
Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id").
|
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
|
||||||
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
Where("pfk.closed_at IS NULL")
|
||||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
|
||||||
Where("r.deleted_at IS NULL")
|
|
||||||
|
|
||||||
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
query = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
||||||
Order("k.id ASC")
|
Order("k.id ASC")
|
||||||
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
if err := query.Scan(&rows).Error; err != nil {
|
||||||
@@ -93,41 +117,44 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
||||||
var rows []HppPerKandangCostRow
|
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()
|
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||||
transferStockableKey := fifo.StockableKeyStockTransferIn.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).
|
query := r.db.WithContext(ctx).
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select(`
|
Select(`
|
||||||
k.id AS kandang_id,
|
k.id AS kandang_id,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS feed_cost,
|
END), 0) AS feed_cost,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS ovk_cost`,
|
END), 0) AS ovk_cost`,
|
||||||
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
||||||
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_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 kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
||||||
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.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 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).
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
||||||
@@ -136,11 +163,10 @@ 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 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 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).
|
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 (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
Where("r.deleted_at IS NULL")
|
Where("r.deleted_at IS NULL").
|
||||||
|
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
|
||||||
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
|
||||||
|
|
||||||
query = query.Group("k.id").Order("k.id ASC")
|
query = query.Group("k.id").Order("k.id ASC")
|
||||||
|
|
||||||
@@ -172,9 +198,8 @@ 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 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 purchases AS pur ON pur.id = pi.purchase_id").
|
||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||||
Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
Group("pfk.kandang_id, s.id, s.name, s.alias")
|
Group("pfk.kandang_id, s.id, s.name, s.alias")
|
||||||
docQuery = applyLocationFilters(docQuery, areaIDs, locationIDs, kandangIDs)
|
|
||||||
|
|
||||||
if err := docQuery.Scan(&docRows).Error; err != nil {
|
if err := docQuery.Scan(&docRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -254,9 +279,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_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 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).
|
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
|
||||||
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
||||||
Group("k.id")
|
Group("k.id")
|
||||||
budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -288,9 +313,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("JOIN locations AS loc ON loc.id = k.location_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_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
|
||||||
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
||||||
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
||||||
Group("k.id")
|
Group("k.id")
|
||||||
expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -323,10 +348,10 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
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).
|
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("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
|
||||||
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
Where("r.deleted_at IS NULL")
|
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 {
|
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -347,6 +372,61 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
return rows, supplierRows, nil
|
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
|
||||||
|
EggProductionWeightKg float64
|
||||||
|
EggProductionPieces 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.weight), 0) AS egg_production_weight_kg,
|
||||||
|
COALESCE(SUM(re.qty), 0) AS egg_production_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 >= ? AND r.record_datetime < ?", start, 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,
|
||||||
|
EggProductionWeightKg: row.EggProductionWeightKg,
|
||||||
|
EggProductionPieces: row.EggProductionPieces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
|
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
|
||||||
if len(areaIDs) > 0 {
|
if len(areaIDs) > 0 {
|
||||||
query = query.Where("loc.area_id IN ?", areaIDs)
|
query = query.Where("loc.area_id IN ?", areaIDs)
|
||||||
|
|||||||
@@ -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("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
|
||||||
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang)
|
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("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
|
||||||
|
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -20,9 +21,10 @@ import (
|
|||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
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"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/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"
|
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
@@ -42,21 +44,25 @@ type RepportService interface {
|
|||||||
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
|
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
|
||||||
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
|
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
|
||||||
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, 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 {
|
type repportService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
DB *gorm.DB
|
||||||
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
||||||
PurchaseRepo purchaseRepo.PurchaseRepository
|
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
ChickinRepo chickinRepo.ProjectChickinRepository
|
PurchaseRepo purchaseRepo.PurchaseRepository
|
||||||
RecordingRepo recordingRepo.RecordingRepository
|
ChickinRepo chickinRepo.ProjectChickinRepository
|
||||||
ApprovalSvc approvalService.ApprovalService
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
ApprovalSvc approvalService.ApprovalService
|
||||||
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
||||||
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
||||||
ProductionResultRepo repportRepo.ProductionResultRepository
|
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
||||||
|
ProductionResultRepo repportRepo.ProductionResultRepository
|
||||||
|
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
|
||||||
|
CustomerRepo customerRepo.CustomerRepository
|
||||||
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
||||||
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,7 @@ type HppCostAggregate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRepportService(
|
func NewRepportService(
|
||||||
|
db *gorm.DB,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
|
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
|
||||||
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
@@ -82,22 +89,27 @@ func NewRepportService(
|
|||||||
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
||||||
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
||||||
productionResultRepo repportRepo.ProductionResultRepository,
|
productionResultRepo repportRepo.ProductionResultRepository,
|
||||||
|
customerPaymentRepo repportRepo.CustomerPaymentRepository,
|
||||||
|
customerRepo customerRepo.CustomerRepository,
|
||||||
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
|
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
|
||||||
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
|
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
|
||||||
) RepportService {
|
) RepportService {
|
||||||
return &repportService{
|
return &repportService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
DB: db,
|
||||||
MarketingDeliveryRepo: marketingDeliveryRepo,
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
PurchaseRepo: purchaseRepo,
|
MarketingDeliveryRepo: marketingDeliveryRepo,
|
||||||
ChickinRepo: chickinRepo,
|
PurchaseRepo: purchaseRepo,
|
||||||
RecordingRepo: recordingRepo,
|
ChickinRepo: chickinRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
RecordingRepo: recordingRepo,
|
||||||
PurchaseSupplierRepo: purchaseSupplierRepo,
|
ApprovalSvc: approvalSvc,
|
||||||
DebtSupplierRepo: debtSupplierRepo,
|
PurchaseSupplierRepo: purchaseSupplierRepo,
|
||||||
HppPerKandangRepo: hppPerKandangRepo,
|
DebtSupplierRepo: debtSupplierRepo,
|
||||||
ProductionResultRepo: productionResultRepo,
|
HppPerKandangRepo: hppPerKandangRepo,
|
||||||
|
ProductionResultRepo: productionResultRepo,
|
||||||
|
CustomerPaymentRepo: customerPaymentRepo,
|
||||||
|
CustomerRepo: customerRepo,
|
||||||
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||||
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||||
}
|
}
|
||||||
@@ -308,6 +320,15 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
|
standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
|
||||||
growthDetailCache := make(map[int]*entity.StandardGrowthDetail)
|
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 cumulativeButir int64
|
||||||
var cumulativeKg float64
|
var cumulativeKg float64
|
||||||
for i := range weeklyResults {
|
for i := range weeklyResults {
|
||||||
@@ -317,6 +338,12 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
if weeklyResults[i].StdUniformity == "" {
|
if weeklyResults[i].StdUniformity == "" {
|
||||||
weeklyResults[i].StdUniformity = defaultUniformText
|
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
|
cumulativeButir += weeklyResults[i].ButiranJumlah
|
||||||
weeklyResults[i].TotalButir = cumulativeButir
|
weeklyResults[i].TotalButir = cumulativeButir
|
||||||
@@ -390,6 +417,205 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
return weeklyResults, totalWeeks, nil
|
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 {
|
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
||||||
result := dto.ProductionResultDTO{
|
result := dto.ProductionResultDTO{
|
||||||
CreatedAt: record.CreatedAt,
|
CreatedAt: record.CreatedAt,
|
||||||
@@ -534,6 +760,68 @@ func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
|
|||||||
return "", false
|
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 {
|
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
|
||||||
if groupSize <= 0 || len(daily) == 0 {
|
if groupSize <= 0 || len(daily) == 0 {
|
||||||
return daily
|
return daily
|
||||||
@@ -1057,10 +1345,37 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
|
|
||||||
if err != nil {
|
validPfkIDs := make([]uint, 0, len(repoRows))
|
||||||
return nil, nil, err
|
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 := 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].EggProductionWeightKg = egg.EggProductionWeightKg
|
||||||
|
repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
||||||
for _, row := range costRows {
|
for _, row := range costRows {
|
||||||
costMap[row.KandangID] = HppCostAggregate{
|
costMap[row.KandangID] = HppCostAggregate{
|
||||||
@@ -1113,9 +1428,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
Max float64
|
Max float64
|
||||||
}
|
}
|
||||||
type weightRangeAggregate struct {
|
type weightRangeAggregate struct {
|
||||||
Summary *dto.HppPerKandangSummaryWeightRangeDTO
|
Summary *dto.HppPerKandangSummaryWeightRangeDTO
|
||||||
EggHppSum float64
|
RemainingBirds int64
|
||||||
EggHppCount int
|
RemainingWeightKg float64
|
||||||
|
AvgWeightSum float64
|
||||||
|
AvgWeightCount int64
|
||||||
|
EggHppSum float64
|
||||||
|
EggHppCount int
|
||||||
|
FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO
|
||||||
|
DocSuppliers map[int64]dto.HppPerKandangSupplierDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
||||||
@@ -1132,8 +1453,14 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
var totalDocPriceCount int
|
var totalDocPriceCount int
|
||||||
var totalEggHppSum float64
|
var totalEggHppSum float64
|
||||||
var totalEggHppCount int
|
var totalEggHppCount int
|
||||||
|
var totalAvgWeightSum float64
|
||||||
|
var totalAvgWeightCount int64
|
||||||
|
|
||||||
for _, row := range repoRows {
|
for _, row := range repoRows {
|
||||||
|
if !params.ShowUnrecorded && row.RecordingCount == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
birdsFloat := row.RemainingChickenBirds
|
birdsFloat := row.RemainingChickenBirds
|
||||||
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
||||||
birdsFloat = 0
|
birdsFloat = 0
|
||||||
@@ -1152,9 +1479,16 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
avgWeight := 0.0
|
avgWeight := 0.0
|
||||||
if birdsFloat > 0 {
|
if eggPiecesFloat > 0 {
|
||||||
avgWeight = weightFloat / birdsFloat
|
avgWeight = eggWeightFloat / eggPiecesFloat
|
||||||
}
|
}
|
||||||
|
if params.WeightMin != nil && avgWeight < *params.WeightMin {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if params.WeightMax != nil && avgWeight > *params.WeightMax {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
weightMin := math.Floor(avgWeight*10) / 10
|
weightMin := math.Floor(avgWeight*10) / 10
|
||||||
if weightMin < 0 {
|
if weightMin < 0 {
|
||||||
weightMin = 0
|
weightMin = 0
|
||||||
@@ -1201,9 +1535,7 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
WeightMin: weightMin,
|
WeightMin: weightMin,
|
||||||
WeightMax: weightMax,
|
WeightMax: weightMax,
|
||||||
},
|
},
|
||||||
RemainingChickenBirds: rowBirds,
|
AvgWeightKg: avgWeight,
|
||||||
RemainingChickenWeightKg: weightFloat,
|
|
||||||
AvgWeightKg: avgWeight,
|
|
||||||
// FeedCostRp: costEntry.FeedCost,
|
// FeedCostRp: costEntry.FeedCost,
|
||||||
// OvkCostRp: costEntry.OvkCost,
|
// OvkCostRp: costEntry.OvkCost,
|
||||||
DocSuppliers: docSupplierMap[row.KandangID],
|
DocSuppliers: docSupplierMap[row.KandangID],
|
||||||
@@ -1211,10 +1543,10 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
EggProductionPieces: rowEggPieces,
|
EggProductionPieces: rowEggPieces,
|
||||||
EggProductionKg: eggWeightFloat,
|
EggProductionKg: eggWeightFloat,
|
||||||
AverageDocPriceRp: avgDocPrice,
|
AverageDocPriceRp: avgDocPrice,
|
||||||
HppRp: hppRp,
|
// HppRp: hppRp,
|
||||||
EggHppRpPerKg: eggHpp,
|
EggHppRpPerKg: eggHpp,
|
||||||
RemainingValueRp: rowRemainingValue,
|
RemainingValueRp: rowRemainingValue,
|
||||||
EggValueRp: rowEggValue,
|
EggValueRp: rowEggValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
totalBirds += rowBirds
|
totalBirds += rowBirds
|
||||||
@@ -1223,6 +1555,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
totalEggKg += eggWeightFloat
|
totalEggKg += eggWeightFloat
|
||||||
totalRemainingValueRp += rowRemainingValue
|
totalRemainingValueRp += rowRemainingValue
|
||||||
totalEggValueRp += rowEggValue
|
totalEggValueRp += rowEggValue
|
||||||
|
totalAvgWeightSum += avgWeight
|
||||||
|
totalAvgWeightCount++
|
||||||
if weightFloat > 0 {
|
if weightFloat > 0 {
|
||||||
totalHppSum += hppRp
|
totalHppSum += hppRp
|
||||||
totalHppCount++
|
totalHppCount++
|
||||||
@@ -1246,13 +1580,27 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
},
|
},
|
||||||
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
|
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
|
||||||
},
|
},
|
||||||
|
FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
|
||||||
|
DocSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
|
||||||
}
|
}
|
||||||
perRangeMap[rangeKey] = rangeAgg
|
perRangeMap[rangeKey] = rangeAgg
|
||||||
}
|
}
|
||||||
|
|
||||||
rangeSummary := rangeAgg.Summary
|
rangeSummary := rangeAgg.Summary
|
||||||
rangeSummary.RemainingChickenBirds += rowBirds
|
rangeAgg.RemainingBirds += rowBirds
|
||||||
rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight
|
rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
||||||
|
rangeAgg.AvgWeightSum += avgWeight
|
||||||
|
rangeAgg.AvgWeightCount++
|
||||||
|
for _, supplier := range feedSupplierMap[row.KandangID] {
|
||||||
|
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
|
||||||
|
rangeAgg.FeedSuppliers[supplier.ID] = supplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, supplier := range docSupplierMap[row.KandangID] {
|
||||||
|
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
|
||||||
|
rangeAgg.DocSuppliers[supplier.ID] = supplier
|
||||||
|
}
|
||||||
|
}
|
||||||
rangeSummary.EggProductionPieces += rowEggPieces
|
rangeSummary.EggProductionPieces += rowEggPieces
|
||||||
rangeSummary.EggProductionKg += eggWeightFloat
|
rangeSummary.EggProductionKg += eggWeightFloat
|
||||||
rangeSummary.RemainingValueRp += rowRemainingValue
|
rangeSummary.RemainingValueRp += rowRemainingValue
|
||||||
@@ -1279,31 +1627,37 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
agg := perRangeMap[key]
|
agg := perRangeMap[key]
|
||||||
entry := agg.Summary
|
entry := agg.Summary
|
||||||
entry.ID = idx + 1
|
entry.ID = idx + 1
|
||||||
if entry.RemainingChickenBirds > 0 {
|
if agg.AvgWeightCount > 0 {
|
||||||
entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds)
|
entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount)
|
||||||
}
|
}
|
||||||
if agg.EggHppCount > 0 {
|
if agg.EggHppCount > 0 {
|
||||||
entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
|
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)
|
perRangeSummary = append(perRangeSummary, *entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSummary := dto.HppPerKandangSummaryTotalDTO{
|
totalSummary := dto.HppPerKandangSummaryTotalDTO{
|
||||||
TotalRemainingChickenBirds: totalBirds,
|
TotalEggProductionPieces: totalEggPieces,
|
||||||
TotalRemainingChickenWeightKg: totalWeight,
|
TotalEggProductionKg: totalEggKg,
|
||||||
TotalEggProductionPieces: totalEggPieces,
|
TotalEggValueRp: totalEggValueRp,
|
||||||
TotalEggProductionKg: totalEggKg,
|
|
||||||
TotalRemainingValueRp: totalRemainingValueRp,
|
|
||||||
TotalEggValueRp: totalEggValueRp,
|
|
||||||
}
|
}
|
||||||
if totalBirds > 0 {
|
if totalBirds > 0 {
|
||||||
totalSummary.AverageWeightKg = totalWeight / float64(totalBirds)
|
}
|
||||||
|
if totalAvgWeightCount > 0 {
|
||||||
|
totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount)
|
||||||
}
|
}
|
||||||
if totalEggHppCount > 0 {
|
if totalEggHppCount > 0 {
|
||||||
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
|
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
|
||||||
}
|
}
|
||||||
if totalHppCount > 0 {
|
if totalHppCount > 0 {
|
||||||
totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount)
|
|
||||||
}
|
}
|
||||||
if totalDocPriceCount > 0 {
|
if totalDocPriceCount > 0 {
|
||||||
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
|
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
|
||||||
|
|||||||
@@ -71,3 +71,11 @@ type ProductionResultQuery struct {
|
|||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
ProjectFlockKandangID uint `query:"-" validate:"required,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"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user